<?php
/**
 * Class WC_Shipstation file.
 * Main class of the plugin.
 *
 * @package WC_ShipStation
 */

namespace WooCommerce\Shipping\ShipStation;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

use Automattic\WooCommerce\Utilities\FeaturesUtil;
use WooCommerce\Shipping\ShipStation\Checkout\Checkout_Rates_Classic_Label;
use WooCommerce\Shipping\ShipStation\Checkout\Checkout_Rates_Options;
use WooCommerce\Shipping\ShipStation\Checkout\Checkout_Rates_Shipping_Method;
use WooCommerce\Shipping\ShipStation\REST_API_Loader;
use WC_ShipStation_Privacy;
use WC_Shipstation_API;

/**
 * WC_Shipstation Class
 */
class Main {
	/**
	 * Instance to call certain functions globally within the plugin
	 *
	 * @var Main|null
	 */
	protected static ?Main $instance = null;

	/**
	 * WPCOM connection facade. Null until the feature flag enables it.
	 *
	 * @var WPCOM_Connection|null
	 */
	protected ?WPCOM_Connection $wpcom_connection = null;

	/**
	 * Main Websparks People Singleton.
	 *
	 * Ensures only one instance is loaded or can be loaded.
	 *
	 * @static
	 * @return self Main instance.
	 */
	public static function instance() {
		if ( is_null( self::$instance ) ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Class constructor.
	 */
	public function __construct() {
		add_action( 'plugins_loaded', array( $this, 'init' ) );
		add_filter( 'woocommerce_integrations', array( $this, 'load_integration' ) );
		add_action( 'woocommerce_api_wc_shipstation', array( $this, 'load_api' ) );
		add_filter( 'plugin_action_links_' . plugin_basename( WC_SHIPSTATION_FILE ), array( $this, 'api_plugin_action_links' ) );
		add_action( 'before_woocommerce_init', array( $this, 'declare_hpos_compatibility' ) );
		add_action( 'woocommerce_refund_created', array( $this, 'save_refund_meta_data' ), 10, 2 );
	}

	/**
	 * WooCommerce fallback notice.
	 *
	 * @since 4.1.26
	 *
	 * @return void
	 */
	public function missing_wc_notice() {
		/* translators: %s WC download URL link. */
		echo '<div class="error"><p><strong>' . sprintf( esc_html__( 'Shipstation requires WooCommerce to be installed and active. You can download %s here.', 'woocommerce-shipstation-integration' ), '<a href="https://woocommerce.com/" target="_blank">WooCommerce</a>' ) . '</strong></p></div>';
	}

	/**
	 * Include shipstation class.
	 *
	 * @since 1.0.0
	 */
	public function init() {
		if ( ! class_exists( 'WooCommerce' ) ) {
			add_action( 'admin_notices', array( $this, 'missing_wc_notice' ) );
			return;
		}

		if ( ! defined( 'WC_SHIPSTATION_EXPORT_LIMIT' ) ) {
			define( 'WC_SHIPSTATION_EXPORT_LIMIT', 100 );
		}

		$this->load_files();
		$this->maybe_init_wpcom_connection();

		// Create/upgrade the ShipStation connection-log table when needed
		// (version-gated; a no-op once installed).
		Connection_Log::maybe_install();

		// Wire the daily Action Scheduler job that prunes never-used plugin API
		// keys (SHIPSTN-142 credentials redesign). Registers its handler and
		// schedules the recurring action on `init`.
		Auth_Controller::register_orphan_prune();

		add_action( 'before_woocommerce_init', array( $this, 'before_woocommerce_init' ) );
		add_action( 'woocommerce_init', array( $this, 'load_rest_api' ) );

		add_filter( 'woocommerce_shipping_methods', array( $this, 'register_shipping_methods' ) );
		add_filter( 'woocommerce_hidden_order_itemmeta', array( $this, 'hide_internal_order_item_meta' ) );

		// Toggling Checkout Rates on or off must invalidate WC's cached package
		// rates, otherwise checkout keeps serving rates calculated while the
		// feature was on (or omits them after it is turned back on).
		add_action(
			'update_option_woocommerce_shipstation_settings',
			array( Checkout_Rates_Options::class, 'flush_shipping_cache_on_settings_change' ),
			10,
			2
		);

		// Moving the store across the supported-country boundary turns Checkout Rates
		// on or off just as the toggle does, and WC regenerates no shipping cache of
		// its own when the base country changes.
		add_action(
			'update_option_woocommerce_default_country',
			array( Checkout_Rates_Options::class, 'flush_shipping_cache_on_base_country_change' ),
			10,
			2
		);

		// Any shipping calculation that runs outside a checkout context (a classic
		// add-to-cart page render, the wc-ajax=get_refreshed_fragments call that trails an
		// AJAX add-to-cart, an unrelated front-end hit that mutates the cart) makes WC cache
		// a package rate set with no ShipStation rate, which checkout then reuses via the
		// matching package hash (SHIPSTN-157). Note when WC caches a set, then discard it on
		// shutdown — before WC_Session_Handler persists the session (its save runs on
		// shutdown at priority 20) — unless it was produced in a checkout context. Shutdown
		// is the one point that runs after every rate-caching path in the request, so a
		// poisoned set can never reach the database and be replayed at checkout.
		add_filter(
			'woocommerce_package_rates',
			array( Checkout_Rates_Options::class, 'note_package_rates_calculated' )
		);
		add_action(
			'shutdown',
			array( Checkout_Rates_Options::class, 'discard_non_checkout_rate_cache' ),
			5
		);

		// Surface each ShipStation checkout rate's delivery estimate and description on the
		// classic (shortcode) cart/checkout label. Registered unconditionally so it is not
		// dropped on sites that enable the feature flag after this runs (e.g. a theme's
		// functions.php); the callback no-ops for non-ShipStation rates.
		Checkout_Rates_Classic_Label::register();
	}

	/**
	 * Bootstrap the WPCOM/Jetpack connection when the feature flag is on.
	 *
	 * @return void
	 */
	protected function maybe_init_wpcom_connection(): void {
		// The transport toggle gates whether ShipStation requests are *routed*
		// through WordPress.com. In the admin, though, always make the connection
		// available so the settings tab can render — and operate — the
		// connect/disconnect controls regardless of the toggle (the controls are
		// CSS-hidden until the checkbox is ticked, so the status is "already there"
		// when it is enabled). The bootstrap's admin hooks all no-op without a
		// pending connect/disconnect action, so this is safe on any admin page.
		// Frontend and REST stay gated by the toggle, preserving request routing.
		if ( ! Features::is_wpcom_transport_enabled() && ! is_admin() ) {
			return;
		}

		// get_wpcom_connection() re-runs this lazily, so a caller can reach it
		// before load_files() has required the class (early boot or a partial
		// install). Stay at "no facade" rather than fataling.
		if ( ! class_exists( WPCOM_Connection::class ) ) {
			return;
		}

		$this->wpcom_connection = new WPCOM_Connection();
		$this->wpcom_connection->bootstrap();
	}

	/**
	 * WPCOM connection facade accessor.
	 *
	 * Re-runs the gated init when the facade is missing: the settings-checkbox
	 * source of the feature flag can turn on after plugins_loaded (the settings
	 * save persists mid-request), in which case maybe_init_wpcom_connection()
	 * already skipped. Late bootstrap still wires this request's admin hooks;
	 * Jetpack's own plugins_loaded-time configuration completes on the next load.
	 *
	 * @return WPCOM_Connection|null Null when the feature flag is off.
	 */
	public function get_wpcom_connection(): ?WPCOM_Connection {
		if ( null === $this->wpcom_connection ) {
			$this->maybe_init_wpcom_connection();
		}

		return $this->wpcom_connection;
	}

	/**
	 * Run instances on time.
	 *
	 * @since 4.4.6
	 */
	public function before_woocommerce_init() {
		new WC_ShipStation_Privacy();
	}

	/**
	 * Include needed files.
	 *
	 * @since 4.4.5
	 */
	public function load_files() {
		// Loaded first: the integration constructed on `init` and the settings
		// data dereference WooCommerce enums through this helper, which falls back
		// to slugs on WooCommerce versions that predate those enums (SHIPSTN-152).
		require_once WC_SHIPSTATION_ABSPATH . 'includes/class-enum-helper.php';
		require_once WC_SHIPSTATION_ABSPATH . 'includes/class-features.php';
		require_once WC_SHIPSTATION_ABSPATH . 'includes/class-order-util.php';
		require_once WC_SHIPSTATION_ABSPATH . 'includes/class-connection-log.php';
		require_once WC_SHIPSTATION_ABSPATH . 'includes/class-wpcom-connection.php';
		include_once WC_SHIPSTATION_ABSPATH . 'includes/class-wc-shipstation-integration.php';
		include_once WC_SHIPSTATION_ABSPATH . 'includes/class-auth-controller.php';
		include_once WC_SHIPSTATION_ABSPATH . 'includes/class-global-connection-banner.php';
		include_once WC_SHIPSTATION_ABSPATH . 'includes/class-logger.php';

		// Options class is side-effect-free and is reached from data-settings.php
		// regardless of the feature flag's timing, so load it unconditionally.
		// It owns SHIPPING_METHOD_ID, so the settings gate and zone-method
		// lookups need nothing from the shipping method class. The shipping
		// method and the rest of the Checkout Rates infrastructure (validator,
		// builder, mapper, API client) load lazily in register_shipping_methods()
		// because they're only needed when actually calculating rates at checkout.
		include_once WC_SHIPSTATION_ABSPATH . 'includes/checkout/class-checkout-rates-options.php';

		// Classic-checkout label formatter. Like the options class it is lightweight and
		// side-effect-free (no WC_Shipping_Method parent), so it loads unconditionally and
		// its filter callback stays resolvable no matter when the feature flag is toggled.
		include_once WC_SHIPSTATION_ABSPATH . 'includes/checkout/class-checkout-rates-classic-label.php';

		include_once WC_SHIPSTATION_ABSPATH . 'includes/class-wc-shipstation-privacy.php';
		include_once WC_SHIPSTATION_ABSPATH . 'includes/class-wc-shipstation-api.php';

		// Load REST API loader class file.
		require_once WC_SHIPSTATION_ABSPATH . 'includes/class-rest-api-loader.php';

		// Include the Checkout class if WooCommerce version is 9.7.0 or higher.
		// This class is used to handle the gift message feature in the checkout process.
		if ( version_compare( WC()->version, '9.7.0', '>=' ) ) {
			include_once WC_SHIPSTATION_ABSPATH . 'includes/class-checkout.php';
		}
	}

	/**
	 * Initialize REST API.
	 *
	 * @since 4.5.2
	 */
	public function load_rest_api() {
		// Initialize REST API.
		$rest_loader = new REST_API_Loader();
		$rest_loader->init();
	}

	/**
	 * Define integration.
	 *
	 * @since 1.0.0
	 *
	 * @param array $integrations Integrations.
	 *
	 * @return array Integrations.
	 */
	public function load_integration( $integrations ) {
		$integrations[] = 'WC_ShipStation_Integration';

		return $integrations;
	}

	/**
	 * Register ShipStation shipping methods.
	 *
	 * @since 4.9.6
	 *
	 * @param array $methods Registered shipping methods.
	 *
	 * @return array Shipping methods.
	 */
	public function register_shipping_methods( array $methods ): array {
		if ( ! Features::is_checkout_rates_enabled() ) {
			return $methods;
		}

		// Checkout Rates can only be provisioned over the REST API — ShipStation pushes
		// the rates URL via a REST endpoint and there is no XML path. Without a stored
		// rates URL the method can never return rates, so don't offer it in the shipping
		// zone's method list. This and the enabled check above mirror the runtime gates in
		// Checkout_Rates_Shipping_Method::calculate_shipping().
		if ( ! Checkout_Rates_Options::is_configured() ) {
			return $methods;
		}

		require_once WC_SHIPSTATION_ABSPATH . 'includes/checkout/interface-checkout-rates-api-client.php';
		require_once WC_SHIPSTATION_ABSPATH . 'includes/checkout/class-shipstation-unit-converter.php';
		require_once WC_SHIPSTATION_ABSPATH . 'includes/checkout/class-checkout-rates-invalid-payload-exception.php';
		require_once WC_SHIPSTATION_ABSPATH . 'includes/checkout/class-checkout-rates-payload-validator.php';
		require_once WC_SHIPSTATION_ABSPATH . 'includes/checkout/class-checkout-rates-request-builder.php';
		require_once WC_SHIPSTATION_ABSPATH . 'includes/checkout/class-checkout-rates-response-mapper.php';
		require_once WC_SHIPSTATION_ABSPATH . 'includes/checkout/class-checkout-rates-api-client.php';
		require_once WC_SHIPSTATION_ABSPATH . 'includes/checkout/class-checkout-rates-shipping-method.php';

		$methods['shipstation_checkout_rates'] = Checkout_Rates_Shipping_Method::class;

		return $methods;
	}

	/**
	 * Listen for API requests.
	 *
	 * @since 1.0.0
	 */
	public function load_api() {
		new WC_Shipstation_API();
	}

	/**
	 * Save refund meta data.
	 *
	 * @since 4.9.5
	 *
	 * @param int   $refund_id Refund ID.
	 * @param array $args Refund arguments.
	 */
	public function save_refund_meta_data( $refund_id, $args ) {
		$refund = wc_get_order( $refund_id );

		if ( ! $refund || ! $refund->get_parent_id() ) {
			return;
		}

		$refund->update_meta_data( '_wc_shipstation_refund_args', $args );
		$refund->save_meta_data();
	}

	/**
	 * Added ShipStation custom plugin action links.
	 *
	 * @since 4.1.17
	 * @version 4.1.17
	 *
	 * @param array $links Links.
	 *
	 * @return array Links.
	 */
	public function api_plugin_action_links( $links ) {
		$setting_link = admin_url( 'admin.php?page=wc-settings&tab=integration&section=shipstation' );
		$plugin_links = array(
			'<a href="' . esc_url( $setting_link ) . '">' . __( 'Settings', 'woocommerce-shipstation-integration' ) . '</a>',
			'<a href="https://woocommerce.com/my-account/tickets">' . __( 'Support', 'woocommerce-shipstation-integration' ) . '</a>',
			'<a href="https://docs.woocommerce.com/document/shipstation-for-woocommerce/">' . __( 'Docs', 'woocommerce-shipstation-integration' ) . '</a>',
		);

		return array_merge( $plugin_links, $links );
	}

	/**
	 * Declaring HPOS compatibility.
	 */
	public function declare_hpos_compatibility() {
		if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) {
			FeaturesUtil::declare_compatibility( 'custom_order_tables', WC_SHIPSTATION_FILE, true );
		}
	}

	/**
	 * Hide ShipStation Checkout Rates internal shipping-item meta from the admin
	 * order screen and orders list table. The underscore prefix already hides
	 * these from the storefront, emails, and the Store API; wp-admin renders item
	 * meta with an empty hide-prefix, so it needs the allow-list filter instead.
	 *
	 * @since 5.0.9
	 *
	 * @param array $hidden Hidden order item meta keys.
	 *
	 * @return array
	 */
	public function hide_internal_order_item_meta( array $hidden ): array {
		$hidden[] = Checkout_Rates_Options::RATE_CODE_META_KEY;
		$hidden[] = Checkout_Rates_Options::QUOTE_ID_META_KEY;

		return $hidden;
	}
}
