<?php
/*
 * Plugin Name:       Live Carts for WooCommerce: Track Real-Time, Abandoned, and Converted Carts!
 * Version:           1.2.12
 * Description:       Monitor your customers' current and past WooCommerce shopping carts via the WordPress admin.
 * Author:            BerryPress
 * Plugin URI:        https://berrypress.com/
 * License:           GPLv3+
 * License URI:       https://www.gnu.org/licenses/gpl-3.0.html
 * Requires Plugins:  woocommerce
 * Text Domain:       live-carts-for-woocommerce
 * GitHub Plugin URI: BerryPress/live-carts-for-woocommerce
 * Requires at least: 6.2
 * Tested up to:      7.0
 * Requires PHP:      7.0
 * WC requires at least: 7.0
 * WC tested up to:   10.8.0
 */

namespace BerryPress\LiveCarts;

defined( 'ABSPATH' ) || exit;

class LiveCarts {
	const VERSION = '1.2.12', CART_ABANDON_TIME = 7200, CART_ARCHIVE_DAYS = 30, MAX_CARTS_WITH_REVISIONS = 100, ADMIN_CAPABILITY = 'manage_woocommerce';

	private $currentCart, $currentCartId;
	private static $instance;

	public static function instance() {
		if (!isset(self::$instance)) {
			self::$instance = new self();
		}
		return self::$instance;
	}
	public static $plugin_url;

	public function __construct() {

		self::$plugin_url = plugin_dir_url( __FILE__ );
		// Declare HPOS compatibility
		add_action('before_woocommerce_init', function(){
			if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
				\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
			}
		});

		add_action('current_screen', [$this, 'onCurrentScreen']);
		add_filter('set_screen_option_berrypress_page_live-carts-for-woocommerce_per_page', [$this, 'filterPerPageScreenOption'], 10, 3);
		add_action('admin_menu', [$this, 'onAdminMenu']);
		
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- the hooked function will check nonce before taking action
		if (isset($_POST['phplugins_carts_settings_save'])) {
			add_action('admin_init', [$this, 'saveSettings']);
		}
		
		add_action('phplugins_livecarts_hourly', [$this, 'hourlyScheduledTasks']);
		add_action('wp_ajax_phplugins_carts_rerun_migration', [$this, 'rerunMigration']);
		add_action('wp_ajax_phplugins_carts_download_debug_log', [$this, 'downloadDebugLog']);

		add_action('admin_enqueue_scripts', [$this, 'adminScripts']);
		
		add_action('rest_api_init', function() {
			require_once(__DIR__.'/includes/stats-controller.php');
			StatsController::register();
		});

		if (is_admin()) {
			// Load BerryPress admin framework only once across all plugins to prevent class redeclaration errors.
			if (!class_exists('BerryPressLiveCarts\\Admin\\Page')) {
				require_once(__DIR__.'/includes/berrypress-admin-framework/Page.php');
				\BerryPressLiveCarts\Admin\Page::maybeRegisterBerryPressAdminMenu();
			}
			require_once(__DIR__.'/includes/analytics.php');
			new Analytics();
		}
		
		$lastSeenVersion = get_option('phplugins_carts_version', '1.0.5');
		if ($lastSeenVersion !== self::VERSION) {
			add_action('init', [$this, 'handleVersionUpgrade'], 1);
		}

		// Add Settings link on Plugins screen (single site and network)
		add_filter('plugin_action_links_'.plugin_basename(__FILE__), [$this, 'addPluginActionLinks']); 
		
		// Improve compatibility with woocommerce-paypal-payments plugin
		add_action('wc_ajax_ppc-simulate-cart', [$this, 'disableCartProcessing'], 1);
		
		// Critical cart processing hooks - these are unhooked in disableCartProcessing() and have exception/error handling
		add_action('woocommerce_cart_loaded_from_session', [$this, 'onCartLoaded']);
		add_action('woocommerce_cart_reset', [$this, 'requestUpdateCartContents']);
		add_action('woocommerce_after_calculate_totals', [$this, 'requestUpdateCartContents']);
		add_action('woocommerce_order_status_changed', [$this, 'onOrderStatusChanged'], 10, 4);
		
		add_shortcode('berrypress_live_cart_id', [$this, 'cartIdShortcode']);
		
		add_action('woocommerce_checkout_order_created', [$this, 'handleNewOrder']);
		add_action('woocommerce_store_api_checkout_order_processed', [$this, 'handleNewOrder']);
	}

	public function addPluginActionLinks($links) {
		$settingsUrl = admin_url('admin.php?page=live-carts-for-woocommerce&settings=1');
		$links[] = '<a href="'.esc_url($settingsUrl).'">'.esc_html__('Settings', 'live-carts-for-woocommerce').'</a>';
		return $links;
	}
 
	
	public function handleVersionUpgrade() {
		global $wpdb;
		
		
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- no placeholders needed
		$allTables = $wpdb->get_col('SHOW TABLES');
		if (in_array($wpdb->prefix.'phplugins_carts', $allTables)) {
			require_once(__DIR__.'/includes/setup.php');
			Setup::upgradeDatabaseTables( get_option('phplugins_carts_version', '1.0.5') );
			update_option('phplugins_carts_version', self::VERSION);
		}
	}
	
	public function addCartIdDisplayClass($classes) {
		$classes[] = 'phplugins-live-carts-show-id';
		return $classes;
	}
	
	public function outputCartId() {
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- html output (escaped in function)
		echo( $this->getCartIdFrontendDisplay() );
	}
	
	public function getCartIdFrontendDisplay($before='') {
		return $before.apply_filters('phplugins_live_carts_frontend_cart_id_html',
			'<div class="phplugins-live-carts-cart-id">
				<span>'.esc_html__('Cart ID:', 'live-carts-for-woocommerce').'</span>
				<span>'.esc_html(self::formatCartId($this->currentCartId)).'</span>
			</div>', $this->currentCartId);
	}
	
	public function cartIdShortcode() {
		return $this->currentCartId ? '<span class="phplugins-live-carts-cart-id-inline">'.esc_html(self::formatCartId($this->currentCartId)).'</span>' : '';
	}
	
	public static function formatCartId($cartId) {
		// Formatted cart IDs MUST be HTML-safe!
		return str_pad( strtoupper(dechex($cartId)), 8, '0', STR_PAD_LEFT );
	}
	
	public static function unformatCartId($cartId) {
		return hexdec($cartId);
	}
	
	public function disableCartProcessing() {
		remove_action('woocommerce_cart_loaded_from_session', [$this, 'onCartLoaded']);
		remove_action('woocommerce_after_calculate_totals', [$this, 'requestUpdateCartContents']);
		remove_action('woocommerce_cart_reset', [$this, 'requestUpdateCartContents']);
		remove_action('woocommerce_order_status_changed', [$this, 'onOrderStatusChanged']);
		
		add_filter('phplugins_live_carts_should_create_cart', '__return_false', 999);
	}
	
	public function saveSettings() {
		global $wpdb;
		
		if (current_user_can(self::ADMIN_CAPABILITY)) {
			check_admin_referer('phplugins-carts-settings-save', 'phplugins_carts_settings_save');
			
			if (empty($_POST['phplugins_carts_debug'])) {
				delete_option('phplugins_carts_debug');
				delete_option('phplugins_carts_debug_level');
			} else {
				if (!get_option('phplugins_carts_debug')) {
					update_option('phplugins_carts_debug', sanitize_key(wp_generate_password(20, false)), false);
				}
				// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated against allowlist
				$level = isset($_POST['phplugins_carts_debug_level']) && $_POST['phplugins_carts_debug_level'] === 'all' ? 'all' : 'errors';
				update_option('phplugins_carts_debug_level', $level, false);
			}
			
			if (empty($_POST['phplugins_carts_debug']) || !empty($_POST['phplugins_carts_debug_clear'])) {
				$log = $this->getDebugLogPath();
				if ($log) {
					@wp_delete_file($log);
				}
			}

			if (empty($_POST['phplugins_carts_no_ip'])) {
				delete_option('phplugins_carts_no_ip');
			} else {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->query('UPDATE '.$wpdb->prefix.'phplugins_carts SET ip_address=""');
				update_option('phplugins_carts_no_ip', 1);
			}
			
			if (empty($_POST['phplugins_carts_no_url'])) {
				delete_option('phplugins_carts_no_url');
			} else {
				
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->query('UPDATE '.$wpdb->prefix.'phplugins_carts SET last_url=""');
				update_option('phplugins_carts_no_url', 1);
			}
			
			if (empty($_POST['phplugins_carts_show_id'])) {
				delete_option('phplugins_carts_show_id');
			} else {
				update_option('phplugins_carts_show_id', 1);
			}

			
            do_action( 'phplugins_live_carts_save_settings' );
		}
	}
	
	public function adminScripts() {
		$currentScreen = get_current_screen();
		if ($currentScreen && ($currentScreen->id == 'berrypress_page_live-carts-for-woocommerce' || $currentScreen->id == 'woocommerce_page_wc-admin')) {
			// Enqueue BerryPress Admin Framework styles
			wp_enqueue_style('berrypress-lc-admin-page', plugins_url('includes/berrypress-admin-framework/assets/css/global-admin-page.min.css', __FILE__), ['berrypress-lc-admin-global'], self::VERSION);
			wp_enqueue_script('phplugins-live-carts-admin', plugins_url('assets/js/admin.min.js', __FILE__), ['wp-hooks', 'wp-i18n', 'react'], self::VERSION, true);
			wp_set_script_translations('phplugins-live-carts-admin', 'live-carts-for-woocommerce');
			wp_enqueue_style('phplugins-live-carts-admin', plugins_url('assets/css/admin.min.css', __FILE__), null, self::VERSION);
		}

		wp_enqueue_style('berrypress-lc-admin-global', plugins_url('includes/berrypress-admin-framework/assets/css/global-admin.min.css', __FILE__), [], self::VERSION);
	}
	
	public function frontendScripts() {
		wp_enqueue_style('phplugins-live-carts', plugins_url('assets/css/frontend.min.css', __FILE__), null, self::VERSION);
	}
	
	public function debugLog($message, $file, $line, $level = 'error') {
		try {
			$logPath = $this->getDebugLogPath();
			if (!$logPath || !$message) {
				return;
			}
			
			$debugLevel = get_option('phplugins_carts_debug_level', 'errors');
			if ($debugLevel === 'errors' && $level !== 'error') {
				return;
			}
			
			// Log rotation: truncate to ~500 KB when file exceeds 1 MB
			if (file_exists($logPath) && filesize($logPath) > 1048576) {
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_get_contents, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
				$contents = file_get_contents($logPath);
				$truncated = substr($contents, -512000);
				$newlinePos = strpos($truncated, "\n");
				if ($newlinePos !== false) {
					$truncated = substr($truncated, $newlinePos + 1);
				}
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
				file_put_contents($logPath, "[log truncated]\n" . $truncated);
			}
			
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- cannot readily be used here due to lack of append support
			file_put_contents($logPath, wp_date('c')."\t[".$level."]\t".add_query_arg([])."\t".basename($file).':'.$line."\t".$message."\n", FILE_APPEND);
		} catch (\Exception $ex) {
		} catch (\Error $err) { }
	}
	
	public function getDebugLogPath() {
		$log = get_option('phplugins_carts_debug');
		if ($log) {
			return __DIR__.'/debug-'.str_replace(['/', '\\'], '', $log).'.txt';
		}
		return null;
	}
	
	public function getDebugLogContents($maxLines = 200) {
		$path = $this->getDebugLogPath();
		if (!$path || !file_exists($path)) {
			return '';
		}
		$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
		if (!$lines) {
			return '';
		}
		if (count($lines) > $maxLines) {
			$lines = array_slice($lines, -$maxLines);
		}
		return implode("\n", $lines);
	}
	
	public function downloadDebugLog() {
		if (!current_user_can(self::ADMIN_CAPABILITY)) {
			wp_die();
		}
		check_admin_referer('phplugins_carts_download_debug_log');
		$path = $this->getDebugLogPath();
		if (!$path || !file_exists($path)) {
			wp_die(esc_html__('No debug log found.', 'live-carts-for-woocommerce'));
		}
		header('Content-Type: text/plain');
		header('Content-Disposition: attachment; filename="live-carts-debug.txt"');
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
		readfile($path);
		exit;
	}

	public static function isMigrationNeeded() {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$tableExists = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s", DB_NAME, $wpdb->prefix . 'phplugins_cart_contents_items'));
		return (bool) !$tableExists || !get_option('phplugins_carts_migration_1_2_0_complete');
	}
	
	public function rerunMigration() {
		global $wpdb;
		if (!current_user_can(self::ADMIN_CAPABILITY)) {
			wp_die();
		}
		check_admin_referer('phplugins_carts_rerun_migration');
		
		require_once(__DIR__.'/includes/setup.php');
		delete_option('phplugins_carts_migration_1_2_0_complete');
		
		$wpdb->show_errors = false;
		$wpdb->suppress_errors = true;
		
		try {
			Setup::upgradeDatabaseTables('1.1.9');
		} catch (\Exception $ex) {
			// $ex is the caught exception; collected below.
		} catch (\Error $err) {
			$ex = $err;
		}
		
		$errors = Setup::$dbErrors;
		if (isset($ex)) {
			$errors[] = $ex->getMessage();
		}
		if (!get_option('phplugins_carts_migration_1_2_0_complete')) {
			if (empty($errors)) {
				$errors[] = __('The migration did not complete due to an unknown error.', 'live-carts-for-woocommerce');
			}
			set_transient('phplugins_carts_migration_errors', $errors, 300);
			delete_transient('phplugins_carts_migration_success');
		} else {
			delete_transient('phplugins_carts_migration_errors');
			set_transient('phplugins_carts_migration_success', true, 300);
		}
		
		wp_safe_redirect(admin_url('admin.php?page=live-carts-for-woocommerce&status=1'));
		exit;
	}
	
	public function handleNewOrder($order) {
		try {
			$status = $order->get_status();
			if (in_array($status, $this->getConversionOrderStatuses())) {
				$this->onOrderStatusChanged($order->get_id(), 'pending', $status, $order);
			}
		} catch (\Exception $ex) {
			$this->debugLog($ex->getMessage(), __FILE__, __LINE__);
		} catch (\Error $err) {
			$this->debugLog($err->getMessage(), __FILE__, __LINE__);
		}
	}
	
	public function getConversionOrderStatuses() {
		return array_merge(wc_get_is_paid_statuses(), ['on-hold']);
	}
	
	public function onOrderStatusChanged($orderId, $from, $to, $order) {
		global $wpdb;
		try {
			$this->debugLog('onOrderStatusChanged: order_id=' . $orderId . ', from=' . $from . ', to=' . $to . ', cart_id=' . ($this->currentCartId ?? 'null'), __FILE__, __LINE__, 'info');
			if ($this->currentCart && $from == 'pending' && in_array($to, $this->getConversionOrderStatuses()) && $order->get_cart_hash() == $this->currentCart->get_cart_hash()) {
				
				$this->cartSeen();
				remove_action('wp_footer', [$this, 'cartSeen']);
				$this->updateCartContents();
				remove_action('shutdown', [$this, 'updateCartContents']);
				
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->update(
					$wpdb->prefix.'phplugins_carts',
					[
						'status' => 'converted',
						'order_id' => $order->get_id()
					],
					[ 'cart_id' => $this->currentCartId ],
					[
						'status' => '%s',
						'order_id' => '%d'
					],
					[ 'cart_id' => '%d' ]
				);
				
				WC()->session->set( 'phplugins_cart_converted', true );

				do_action('phplugins_live_carts_cart_converted', $this->currentCartId);
				
				$this->resetCurrentCart();
			}
		
		} catch (\Exception $ex) {
			$this->debugLog($ex->getMessage(), __FILE__, __LINE__);
		} catch (\Error $err) {
			$this->debugLog($err->getMessage(), __FILE__, __LINE__);
		}
	}
	public function filterPerPageScreenOption($f, $screenOption, $perPage) {
		return min(absint($perPage), 999);
	}

	public function getTimestampFormat() {
		return get_option('date_format').' '.get_option('time_format');
	}

	public function onCurrentScreen($screen) {
		if ($screen->id == 'berrypress_page_live-carts-for-woocommerce' && current_user_can(self::ADMIN_CAPABILITY)) {
			$this->hourlyScheduledTasks();

			add_screen_option('per_page');
			require_once(__DIR__.'/includes/admin-page.php');
			
			if ( isset( $_GET['cart_id'] ) || isset( $_GET['settings'] ) || isset( $_GET['addons'] ) || isset( $_GET['healthstatus'] ) ) {
				$table = null;
			} else {
				require_once( __DIR__ . '/includes/admin-list.php' );
				$table = new CartsListTable();
				if ($table->current_action() == 'delete') {
					global $wpdb;
					
					check_admin_referer('bulk-carts');
					$ids = array_map('intval', $_REQUEST['post'] ?? []);
					$idPlaceholders = implode(',', array_fill(0, count($ids), '%d'));
					
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
					$wpdb->query(
						$wpdb->prepare(
							// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- false alarm
							'DELETE FROM '.$wpdb->prefix.'phplugins_cart_contents WHERE cart_id IN ('.$idPlaceholders.')',
							...$ids
						)
					);
					
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
					$wpdb->query(
						$wpdb->prepare(
							// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- false alarm
							'DELETE FROM '.$wpdb->prefix.'phplugins_carts WHERE cart_id IN ('.$idPlaceholders.')',
							...$ids
						)
					);
					
					wp_safe_redirect( remove_query_arg('action') );
					exit;
				}
			}
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
			$adminPageClass = apply_filters('live_carts_for_woocommerce_admin_page_class', AdminPage::class);
			new $adminPageClass($table);

			add_filter('admin_body_class', [$this, 'addAdminBodyClass']);
		}
	}
	
	public function onAdminMenu() {
		add_submenu_page('berrypress', __('Live Carts for WooCommerce', 'live-carts-for-woocommerce'), __('Live Carts', 'live-carts-for-woocommerce'), self::ADMIN_CAPABILITY, 'live-carts-for-woocommerce', '__return_empty_string');
	}

	public function getCartStatuses() {
		return [
			'active' => __('Active', 'live-carts-for-woocommerce'),
			'converted' => __('Converted', 'live-carts-for-woocommerce'),
			'abandoned' => __('Abandoned', 'live-carts-for-woocommerce'),
		];
	}

	public function addAdminBodyClass($classes) {
		$classes .= ' berrypress-page ';

		return $classes;
	}

	public function resetCurrentCart() {
		unset($this->currentCart, $this->currentCartId);
	}

	public function onCartLoaded($cart) {
		try {
			$this->currentCart = $cart;
			$this->currentCartId = WC()->session->get( 'phplugins_cart_id', null );
			$this->debugLog('onCartLoaded: session cart_id=' . ($this->currentCartId ?? 'null') . ', cart_empty=' . ($cart->is_empty() ? 'yes' : 'no') . ', items=' . count($cart->get_cart_contents()), __FILE__, __LINE__, 'info');
			if (empty($this->currentCartId) || !$this->validateCurrentCart()) {
				try {
					$this->createNewCart($cart);
					if (!empty($this->currentCartId)) {
						$this->updateCartContents();
					}
				} catch (\Exception $ex) {
					$this->debugLog($ex->getMessage(), __FILE__, __LINE__);
					return;
				}
			}
			
			$this->maybeAddCartHooks();
			
			do_action('phplugins_live_carts_cart_loaded', $cart);
		} catch (\Exception $ex) {
			$this->debugLog($ex->getMessage(), __FILE__, __LINE__);
		} catch (\Error $err) {
			$this->debugLog($err->getMessage(), __FILE__, __LINE__);
		}
	}
	
	public function maybeAddCartHooks() {
		if (!empty($this->currentCartId) && !has_action('wp_footer', [$this, 'cartSeen'])) {
			add_action('wp_footer', [$this, 'cartSeen']);
		
			if ( get_option('phplugins_carts_show_id') ) {
				add_action('woocommerce_after_cart_table', [$this, 'outputCartId']);
				add_filter('render_block_woocommerce/cart', [$this, 'getCartIdFrontendDisplay']);
				add_filter('render_block_woocommerce/mini-cart-contents', [$this, 'getCartIdFrontendDisplay']);
				add_action('body_class', [$this, 'addCartIdDisplayClass']);
				add_action('wp_enqueue_scripts', [$this, 'frontendScripts']);
			}
		}
	}

	public function validateCurrentCart() {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->get_row( $wpdb->prepare('SELECT status FROM '.$wpdb->prefix.'phplugins_carts WHERE cart_id=%d', $this->currentCartId) );
		$valid = $result && $result->status != 'converted';
		if (!$valid) {
			unset($this->currentCartId);
		}
		return $valid;
	}

	public function createNewCart($cart) {
		global $wpdb;
		
		$isConvertedCart = WC()->session->get( 'phplugins_cart_converted', false );
		if ($isConvertedCart && $cart->is_empty()) {
			WC()->session->set( 'phplugins_cart_converted', null );
		}
		
		if (!apply_filters('phplugins_live_carts_should_create_cart', !$cart->is_empty() && !$isConvertedCart, $cart)) {
			return;
		}
		
			do {
			$cartId = random_int(1, hexdec('FFFFFFFF'));
			
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$foundCartId = $wpdb->get_col($wpdb->prepare('SELECT cart_id FROM '.$wpdb->prefix.'phplugins_carts WHERE cart_id=%d', $cartId));
		} while ($foundCartId);

		list($insertData, $insertDataFormat) = apply_filters('phplugins_live_carts_cart_insert', [
			[
				'cart_id' => $cartId,
				'user_id' => get_current_user_id(),
				'created' => current_time('mysql', true),
				'ip_address' => get_option('phplugins_carts_no_ip') ? '' : apply_filters('phplugins_live_carts_cart_ip_address', sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? ''))),
				'status' => 'active'
			],
			[
				'cart_id' => '%d',
				'user_id' => '%d',
				'created' => '%s',
				'ip_address' => '%s',
				'status' => '%s'
			]
		]);
		
		
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->insert(
			$wpdb->prefix.'phplugins_carts',
			$insertData,
			$insertDataFormat
		);
		
		if (!$result) {
			throw new \Exception( esc_html__('Cart could not be created', 'live-carts-for-woocommerce') );
		}
		
		$this->currentCartId = $cartId;
		WC()->session->set( 'phplugins_cart_id', $this->currentCartId );
		
		$this->debugLog('createNewCart: new cart_id=' . $cartId . ', user_id=' . get_current_user_id(), __FILE__, __LINE__, 'info');

		do_action('phplugins_live_carts_cart_created', $cartId);

		$this->cartSeen();
	}

	public function cartSeen() {
		global $wpdb;
		
		if (empty($this->currentCartId)) {
			return;
		}
		
		try {
			$updateValues = [
				'last_seen' => current_time('mysql', true),
				'status' => 'active',
				'archived' => 0,
				'user_id' => get_current_user_id(),
				'ip_address' => get_option('phplugins_carts_no_ip') ? '' : apply_filters('phplugins_live_carts_cart_ip_address', sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? '')))
			];
		
			$updateFormats = [
				'last_seen' => '%s',
				'status' => '%s',
				'archived' => '%d',
				'user_id' => '%s',
				'ip_address' => '%s'
			];
			
			if (!get_option('phplugins_carts_no_url') && !is_404() && !is_admin() && !wp_doing_ajax() && !wp_is_serving_rest_request()) {
				$updateValues['last_url'] = add_query_arg([]);
				$updateFormats['last_url'] = '%s';
			}
			
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$result = $wpdb->update(
				$wpdb->prefix.'phplugins_carts',
				$updateValues,
				[ 'cart_id' => $this->currentCartId ],
				$updateFormats,
				[ 'cart_id' => '%d' ]
			);
			
			do_action('phplugins_live_carts_cart_seen', $this->currentCartId);

			if ($result === false) {
				throw new \Exception( esc_html__('Cart seen could not be updated', 'live-carts-for-woocommerce') );
			}
		} catch (\Exception $ex) {
			$this->debugLog($ex->getMessage(), __FILE__, __LINE__);
		} catch (\Error $err) {
			$this->debugLog($err->getMessage(), __FILE__, __LINE__);
		}
	}
	
	public function requestUpdateCartContents() {
		if (!has_action('shutdown', [$this, 'updateCartContents'])) {
			add_action('shutdown', [$this, 'updateCartContents']);
		}
	}
	
	public function updateCartContents() {
		global $wpdb;
		
		try {
			if (!isset($this->currentCart)) {
				return;
			}
			
			if (empty($this->currentCartId)) {
				try {
					$this->createNewCart($this->currentCart);
					$this->maybeAddCartHooks();
				} catch (\Exception $ex) {
					$this->debugLog($ex->getMessage(), __FILE__, __LINE__);
					return;
				}
			}
			
			if (empty($this->currentCartId)) {
				return;
			}
			
			// Check if migration has been completed
			if (!get_option('phplugins_carts_migration_1_2_0_complete')) {
				return; // Migration not completed yet, skip cart contents update
			}
			
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$result = $wpdb->update(
				$wpdb->prefix.'phplugins_carts',
				[
					'value' => $this->currentCart->get_total('edit') - $this->currentCart->get_total_tax(),
					'coupon' => implode(', ', $this->currentCart->get_applied_coupons())
				],
				[ 'cart_id' => $this->currentCartId ],
				[
					'value' => '%f',
					'coupon' => '%s'
				],
				[ 'cart_id' => '%d' ]
			);
			
			if ($result === false) {
				throw new \Exception( __('Cart could not be updated', 'live-carts-for-woocommerce') );
			}

			$currentContents = $this->currentCart->get_cart_contents();
			
			$debugItems = [];
			foreach ($currentContents as $item) {
				$debugItems[] = 'pid=' . $item['product_id'] . (empty($item['variation_id']) ? '' : ',vid=' . $item['variation_id']) . ',qty=' . $item['quantity'];
			}
			$this->debugLog('updateCartContents: cart_id=' . $this->currentCartId . ', items=[' . implode('; ', $debugItems) . ']', __FILE__, __LINE__, 'info');

			self::saveCartContents($this->currentCartId, $currentContents) && do_action('phplugins_live_carts_cart_contents_updated', $this->currentCartId);
		} catch (\Exception $ex) {
			$this->debugLog($ex->getMessage(), __FILE__, __LINE__);
		} catch (\Error $err) {
			$this->debugLog($err->getMessage(), __FILE__, __LINE__);
		}
	}

	private static function arraysEqual($arr1, $arr2) {
		if (array_diff_key($arr1, $arr2) || array_diff_key($arr2, $arr1)) {
			return false;
		}
		foreach ($arr1 as $field => $value) {
			if (is_array($value) != is_array($arr2[$field])) {
				return false;
			}
			if (is_array($value)) {
				return self::arraysEqual($value, $arr2[$field]);
			}
			if ($value != $arr2[$field]) {
				return false;
			}
		}
		return true;
	}
		
	public static function saveCartContents($cartId, $currentContents) {
		global $wpdb;
		
			
		// Get the last cart contents to check if changed
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$lastContentsId = $wpdb->get_var( $wpdb->prepare('SELECT contents_id FROM '.$wpdb->prefix.'phplugins_cart_contents WHERE cart_id=%d ORDER BY contents_id DESC LIMIT 1', $cartId) );
		
		$contentsChanged = true;
		if ($lastContentsId) {
			// Check if contents actually changed by comparing items
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$lastItems = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT product_id, variation_id, quantity, line_subtotal, line_subtotal_tax, line_total, line_tax, meta 
					FROM '.$wpdb->prefix.'phplugins_cart_contents_items 
					WHERE contents_id=%d
					ORDER BY item_id ASC',
					$lastContentsId
				),
				ARRAY_A
			);
			
			// Compare with current contents
			$currentContentsArray = [];
			foreach ($currentContents as &$item) {
				$meta = $item;
				unset($meta['product_id'], $meta['variation_id'], $meta['quantity'], 
					  $meta['line_subtotal'], $meta['line_subtotal_tax'], 
					  $meta['line_total'], $meta['line_tax']);
					  
				if (empty($meta['data'])) {
					unset($meta['data']);
				}
				
				$item['_php_lc_meta'] = wp_json_encode($meta);
				
				$currentContentsArray[] = [
					'product_id' => isset($item['product_id']) ? (string) $item['product_id'] : '0',
					'variation_id' => isset($item['variation_id']) ? (string) $item['variation_id'] : '0',
					'quantity' => isset($item['quantity']) ? (string) $item['quantity'] : '0',
					'line_subtotal' => isset($item['line_subtotal']) ? (string) $item['line_subtotal'] : '0',
					'line_subtotal_tax' => isset($item['line_subtotal_tax']) ? (string) $item['line_subtotal_tax'] : '0',
					'line_total' => isset($item['line_total']) ? (string) $item['line_total'] : '0',
					'line_tax' => isset($item['line_tax']) ? (string) $item['line_tax'] : '0',
					'meta' => $item['_php_lc_meta']
				];
			}
			unset($item);
			
			if (count($lastItems) != count($currentContentsArray)) {
				$contentsChanged = true;
			} else {
				$contentsChanged = false;
				foreach ($currentContentsArray as $item) {
					foreach ($lastItems as &$lastItem) {
						if ($lastItem) {
							$itemMatch = true;
							foreach ($item as $itemField => $itemValue) {
								if ($itemField != 'meta' && $itemValue != $lastItem[$itemField]) {
									$itemMatch = false;
									break;
								}
							}
							if ($itemMatch && self::arraysEqual(json_decode($item['meta'], true), json_decode($lastItem['meta'], true))) {
								$lastItem = null;
								continue 2;
							}
						}
					}
					$contentsChanged = true;
					break;
				}
			}
			
		}
		
		if ($contentsChanged) {
			if (apply_filters('phplugins_live_carts_track_revisions', true)) {
				// Insert new cart contents record
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$result = $wpdb->insert(
					$wpdb->prefix.'phplugins_cart_contents',
					[
						'cart_id' => $cartId,
						'ts' => current_time('mysql', true)
					],
					[
						'cart_id' => '%d',
						'ts' => '%s'
					]
				);
				
				if ($result === false) {
					throw new \Exception( esc_html__('Cart contents could not be created', 'live-carts-for-woocommerce') );
				}
				
				$contentsId = $wpdb->insert_id;
			} else {
				// Update existing cart contents record
				if ($lastContentsId) {
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
					$result = $wpdb->update(
						$wpdb->prefix.'phplugins_cart_contents',
						['ts' => current_time('mysql', true)],
						['contents_id' => $lastContentsId],
						['ts' => '%s'],
						['contents_id' => '%d']
					);
					
					// Delete old items
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
					$wpdb->delete(
						$wpdb->prefix.'phplugins_cart_contents_items',
						['contents_id' => $lastContentsId],
						['%d']
					);
					
					$contentsId = $lastContentsId;
				} else {
					// No existing record, create new one
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
					$result = $wpdb->insert(
						$wpdb->prefix.'phplugins_cart_contents',
						[
							'cart_id' => $cartId,
							'ts' => current_time('mysql', true)
						],
						[
							'cart_id' => '%d',
							'ts' => '%s'
						]
					);
					
					if ($result === false) {
						throw new \Exception( esc_html__('Cart contents could not be created', 'live-carts-for-woocommerce') );
					}
					
					$contentsId = $wpdb->insert_id;
				}
			}
			
			// Insert cart items
			foreach ($currentContents as $item) {
				if (!isset($item['_php_lc_meta'])) {
					$meta = $item;
					unset($meta['product_id'], $meta['variation_id'], $meta['quantity'], 
						  $meta['line_subtotal'], $meta['line_subtotal_tax'], 
						  $meta['line_total'], $meta['line_tax']);
						  
					if (empty($meta['data'])) {
						unset($meta['data']);
					}
					
					$item['_php_lc_meta'] = wp_json_encode($meta);
				}
				
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->insert(
					$wpdb->prefix.'phplugins_cart_contents_items',
					[
						'contents_id' => $contentsId,
						'product_id' => isset($item['product_id']) ? (int) $item['product_id'] : 0,
						'variation_id' => isset($item['variation_id']) ? (int) $item['variation_id'] : 0,
						'quantity' => isset($item['quantity']) ? (float) $item['quantity'] : 0,
						'line_subtotal' => isset($item['line_subtotal']) ? (float) $item['line_subtotal'] : 0,
						'line_subtotal_tax' => isset($item['line_subtotal_tax']) ? (float) $item['line_subtotal_tax'] : 0,
						'line_total' => isset($item['line_total']) ? (float) $item['line_total'] : 0,
						'line_tax' => isset($item['line_tax']) ? (float) $item['line_tax'] : 0,
						'meta' => $item['_php_lc_meta']
					],
					['%d', '%d', '%d', '%f', '%f', '%f', '%f', '%f', '%s']
				);
			}
			
			return true;
		}
		return false;
	}

	public function hourlyScheduledTasks() {
		global $wpdb;

		// Abandoned carts
		
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$abandonedCarts = $wpdb->get_col(
			$wpdb->prepare(
				'SELECT cart_id
				FROM '.$wpdb->prefix.'phplugins_carts
				WHERE status="active"
					AND last_seen < %s',
				wp_date('Y-m-d H:i:s', time() - apply_filters('phplugins_live_carts_abandon_time', self::CART_ABANDON_TIME), new \DateTimeZone('UTC'))
			)
		);
			foreach (array_chunk($abandonedCarts, 20) as $abandonedCartsChunk) {
				// Build placeholders string (safe - contains only %d)
				$placeholders = implode(',', array_fill(0, count($abandonedCartsChunk), '%d'));
				// Build SQL string with sprintf, then prepare with values
				$sql = sprintf(
					'UPDATE %sphplugins_carts SET status="abandoned" WHERE cart_id IN (%s)',
					$wpdb->prefix,
					$placeholders
				);
				// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql built with sprintf above, placeholders are safe (%d only)
				$preparedQuery = $wpdb->prepare( $sql, ...$abandonedCartsChunk );
			
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query is prepared above
				$wpdb->query($preparedQuery);
			if (has_action('phplugins_live_carts_cart_abandoned')) {
				array_walk($abandonedCartsChunk, function($cartId) {
					do_action('phplugins_live_carts_cart_abandoned', $cartId);
				});
			}
		}

		// Archived carts
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query(
			$wpdb->prepare(
				'UPDATE '.$wpdb->prefix.'phplugins_carts
				SET archived=1
				WHERE last_seen < %s',
				wp_date('Y-m-d H:i:s', time() - apply_filters('phplugins_live_carts_archive_time', (self::CART_ARCHIVE_DAYS * 86400)), new \DateTimeZone('UTC'))
			)
		);

		// Delete archived/orphaned cart contents
		
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query(
			'DELETE FROM '.$wpdb->prefix.'phplugins_cart_contents
			WHERE NOT EXISTS(
				SELECT 1 FROM '.$wpdb->prefix.'phplugins_carts c
				WHERE c.cart_id='.$wpdb->prefix.'phplugins_cart_contents.cart_id AND NOT c.archived
			)'
		);

		// Maybe clear cart revisions
		$maxCartsWithRevisions = apply_filters('phplugins_live_carts_max_carts_with_revisions', self::MAX_CARTS_WITH_REVISIONS);
		if ($maxCartsWithRevisions) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->query(
				$wpdb->prepare(
					'DELETE cc FROM '.$wpdb->prefix.'phplugins_cart_contents cc
					JOIN (SELECT cart_id, MAX(contents_id) AS contents_id FROM '.$wpdb->prefix.'phplugins_cart_contents GROUP BY cart_id) cc_max ON(cc_max.cart_id=cc.cart_id)
					LEFT JOIN (SELECT cart_id FROM '.$wpdb->prefix.'phplugins_carts ORDER BY last_seen DESC LIMIT %d) c ON(c.cart_id=cc.cart_id)
					WHERE c.cart_id IS NULL AND cc.contents_id != cc_max.contents_id',
					$maxCartsWithRevisions
				)
			);
		}
	}
	
	
	

	/**
	 * Helper method to get cart contents from new table structure.
	 *
	 * @param int $contents_id The contents_id to load items for.
	 * @return array Cart contents array.
	 */
	public static function getCartContentsArray( $contents_id ) {
		global $wpdb;
		
		if ( empty( $contents_id ) ) {
			return array();
		}
		
		// Load from phplugins_cart_contents_items (new structure only - requires FREE 1.2.0+)
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$items = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT product_id, variation_id, quantity, line_subtotal, line_subtotal_tax, line_total, line_tax, meta 
				FROM ' . $wpdb->prefix . 'phplugins_cart_contents_items 
				WHERE contents_id=%d
				ORDER BY item_id ASC',
				$contents_id
			),
			ARRAY_A
		);
		
		$contents = array();
		foreach ( $items as $item ) {
			$meta = json_decode( $item['meta'], true );
			if ( ! is_array( $meta ) ) {
				$meta = array();
			}
			
			// Merge meta with the main item data and use the cart item key
			$key = $meta['key'] ?? (new \WC_Cart())->generate_cart_id( $item['product_id'], $item['variation_id'], $meta['variation'] ?? [], $meta );
			$contents[ $key ] = array_merge(
				$meta,
				array(
					'product_id' => $item['product_id'],
					'variation_id' => $item['variation_id'],
					'quantity' => $item['quantity'],
					'line_subtotal' => $item['line_subtotal'],
					'line_subtotal_tax' => $item['line_subtotal_tax'],
					'line_total' => $item['line_total'],
					'line_tax' => $item['line_tax']
				)
			);
		}
		
		return $contents;
	}
	
	public static function getCartContentsHistory( $cart_id ) {
		global $wpdb;
		
		$history = [];
		
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$revisions = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT contents_id, ts 
				FROM ' . $wpdb->prefix . 'phplugins_cart_contents
				WHERE cart_id=%d
				ORDER BY ts ASC',
				$cart_id
			),
			ARRAY_N
		);
		
		if ($revisions) {
			$products = [];
			$lastContents = [];
			$lastContentItems = [];
			foreach ( $revisions as $revision ) {
				$contents = [];
				$contentItems = [];
				foreach (self::getCartContentsArray($revision[0]) as $item) {
					$itemId = empty( $item['variation_id'] ) ? $item['product_id'] : $item['variation_id'];
					$contents[$itemId] = ($contents[$itemId] ?? 0) + $item['quantity'];
					$contentItems[$itemId] = $item;
				}
				
				$addedItems = array_diff_key($contents, $lastContents);
				$removedItems = array_diff_key($lastContents, $contents);
				$changedItems = array_diff_assoc(array_intersect_key($contents, $lastContents), array_intersect_key($lastContents, $contents));
				
				foreach (array_diff( array_keys($contents), array_keys($products) ) as $itemId) {
					$products[$itemId] = wc_get_product($itemId);
				}
				
				$diffs = [];
				foreach ($addedItems as $itemId => $quantity) {
					$diffs[] = ['add', empty($products[$itemId]) ? null : $products[$itemId], $quantity, $contentItems[$itemId] ?? []];
				}
				foreach ($removedItems as $itemId => $quantity) {
					$diffs[] = ['remove', empty($products[$itemId]) ? null : $products[$itemId], $lastContentItems[$itemId] ?? []];
				}
				foreach ($changedItems as $itemId => $quantity) {
					$diffs[] = ['change-quantity', empty($products[$itemId]) ? null : $products[$itemId], $lastContents[$itemId], $quantity, $contentItems[$itemId] ?? []];
				}
				
				if ($diffs) {
					$history[] = [$revision[1], $diffs];
				}
				$lastContents = $contents;
				$lastContentItems = $contentItems;
			}
		}
		
		return $history;
	}

	public static function onPluginActivate() {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$allTables = $wpdb->get_col('SHOW TABLES');
		if (!in_array($wpdb->prefix.'phplugins_carts', $allTables)) {
			require_once(__DIR__.'/includes/setup.php');
			Setup::createDatabaseTables();
			Setup::upgradeDatabaseTables('1.0.0');
			update_option('phplugins_carts_version', self::VERSION);
		}

		wp_schedule_event(time() + 3600, 'hourly', 'phplugins_livecarts_hourly');
	}

	public static function onPluginDeactivate() {
		wp_clear_scheduled_hook('phplugins_livecarts_hourly');
	}
}

LiveCarts::instance();

register_activation_hook(__FILE__, ['BerryPress\LiveCarts\LiveCarts', 'onPluginActivate']);
register_deactivation_hook(__FILE__, ['BerryPress\LiveCarts\LiveCarts', 'onPluginDeactivate']);
