<?php
/**
 * Abandoned cart CRUD + manual email send.
 *
 * @package SuiteMkt3k
 */

namespace SuiteMkt3k\Modules\Cart_Recovery;

use SuiteMkt3k\Compat\WooCommerce_Check;
use SuiteMkt3k\Email\Email_Renderer;
use SuiteMkt3k\Email\Mailer;
use SuiteMkt3k\Installer;
use SuiteMkt3k\Modules\Settings\Settings_Manager;
use SuiteMkt3k\Modules\Subscribers\Subscriber_Manager;
use SuiteMkt3k\Security\Capabilities;
use SuiteMkt3k\Security\Nonces;
use SuiteMkt3k\Utils\Logger;
use SuiteMkt3k\Utils\Sanitizer;

defined( 'ABSPATH' ) || exit;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching

final class Cart_Recovery_Manager {

	/** WC session key set when the shopper opens a recovery email link. */
	public const SESSION_RECOVERY_CART_ID = 'wmk_recovery_cart_id';

	/** Order meta written at checkout when the session carries a recovery cart. */
	public const ORDER_META_RECOVERY_CART_ID = '_suitemkt3k_recovery_cart_id';

	public static function statuses(): array {
		return array(
			'pending'   => __( 'Pending', 'suitemkt3k' ),
			'emailed'   => __( 'Emailed', 'suitemkt3k' ),
			'recovered' => __( 'Recovered', 'suitemkt3k' ),
			'completed' => __( 'Completed', 'suitemkt3k' ),
			'deleted'   => __( 'Deleted', 'suitemkt3k' ),
			'expired'   => __( 'Expired', 'suitemkt3k' ),
		);
	}

	public static function status_label( string $status ): string {
		$map = self::statuses();
		return $map[ $status ] ?? $status;
	}

	public static function register(): void {
		add_action( 'admin_post_suitemkt3k_cart_template_save', array( __CLASS__, 'handle_template_save' ) );
		add_action( 'admin_post_suitemkt3k_cart_send_email', array( __CLASS__, 'handle_send_email' ) );
		add_action( 'admin_post_suitemkt3k_cart_test_email', array( __CLASS__, 'handle_test_email' ) );
		add_action( 'admin_post_suitemkt3k_cart_delete', array( __CLASS__, 'handle_delete' ) );
		add_action( 'admin_post_suitemkt3k_cart_delete_all', array( __CLASS__, 'handle_delete_all' ) );
		add_action( 'admin_post_suitemkt3k_cart_import_wc', array( __CLASS__, 'handle_import_wc' ) );
		add_action( 'wp_loaded', array( __CLASS__, 'maybe_handle_recovery_link' ), 20 );

		// Persist attribution onto the order, then mark recovered only when attributed.
		add_action( 'woocommerce_checkout_create_order', array( __CLASS__, 'attach_recovery_meta_to_order' ), 20, 2 );
		add_action( 'woocommerce_order_status_processing', array( __CLASS__, 'maybe_mark_recovered' ), 20 );
		add_action( 'woocommerce_order_status_completed', array( __CLASS__, 'maybe_mark_recovered' ), 20 );

		if ( WooCommerce_Check::is_active() ) {
			add_filter( 'woocommerce_coupon_is_valid', array( __CLASS__, 'block_extra_coupons_with_recovery' ), 10, 2 );
		}
	}

	/**
	 * One-time cleanup: carts marked "recovered" without ever receiving a recovery email
	 * were false positives from normal checkouts. Reclassify them as "completed" so stats
	 * stop counting them as recovered revenue. Idempotent via option flag.
	 *
	 * Does not change schema. Does not touch carts that did receive a recovery email.
	 */
	public static function maybe_fix_false_recoveries(): void {
		if ( get_option( 'suitemkt3k_fix_false_cart_recoveries_v1' ) ) {
			return;
		}

		global $wpdb;
		$table = self::table();
		$exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
		if ( $exists !== $table ) {
			return;
		}

		$affected = $wpdb->query(
			$wpdb->prepare(
				"UPDATE %i
				SET status = %s,
					wc_order_id = COALESCE(NULLIF(wc_order_id, 0), recovered_order_id),
					recovered_at = NULL,
					recovered_order_id = NULL
				WHERE status = %s
				AND (recovery_email_sent_at IS NULL OR recovery_email_sent_at = %s OR recovery_email_sent_at = %s)",
				$table,
				'completed',
				'recovered',
				'0000-00-00 00:00:00',
				''
			)
		);

		// Retry on next request if the query failed (e.g. transient DB error).
		if ( false === $affected ) {
			return;
		}

		update_option( 'suitemkt3k_fix_false_cart_recoveries_v1', time(), false );

		if ( $affected > 0 ) {
			Logger::info(
				'cart_recovery',
				'Fixed false recovered carts (no recovery email sent)',
				array( 'rows' => $affected )
			);
		}
	}

	public static function table(): string {
		return Installer::table( 'abandoned_carts' );
	}

	public static function templates_table(): string {
		return Installer::table( 'cart_email_templates' );
	}

	public static function list( int $limit = 100 ): array {
		global $wpdb;
		$limit = max( 1, min( 500, $limit ) );
		$rows = $wpdb->get_results(
			$wpdb->prepare( 'SELECT * FROM %i ORDER BY last_activity_at DESC LIMIT %d', self::table(), $limit ),
			ARRAY_A
		);
		return is_array( $rows ) ? $rows : array();
	}

	public static function get( int $id ): ?array {
		global $wpdb;
		return $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', self::table(), $id ), ARRAY_A ) ?: null;
	}

	/**
	 * Default design fields for cart recovery emails.
	 *
	 * @return array<string, mixed>
	 */
	public static function default_design(): array {
		return array(
			'heading'           => __( 'Hello again!', 'suitemkt3k' ),
			'preheader'         => __( 'Your cart is waiting for you.', 'suitemkt3k' ),
			'body'              => __( 'Hi {{name}}, it is possible that your order did not finish processing and that it may have been our fault. We apologize, we saved your cart so you can finish your purchase whenever you want. Also, we would like to offer you this discount coupon as compensation. Best regards, {{company_name}}', 'suitemkt3k' ),
			'cta_label'         => __( 'Resume my order', 'suitemkt3k' ),
			'accent'            => '#2AB2E5',
			'validity_text'     => __( 'This discount is valid for {{hours}} hours after the email is sent.', 'suitemkt3k' ),
			'unsubscribe_text'  => __( 'You no longer want to receive emails like this?', 'suitemkt3k' ),
			'unsubscribe_cta'   => __( 'Unsubscribe', 'suitemkt3k' ),
			'cta_radius'        => '10',
		);
	}

	/**
	 * @param array<string, mixed> $saved
	 * @return array<string, mixed>
	 */
	public static function merge_design( array $saved ): array {
		$design = self::default_design();
		foreach ( $saved as $key => $value ) {
			if ( is_string( $value ) && '' === trim( $value ) ) {
				continue;
			}
			$design[ $key ] = $value;
		}
		return $design;
	}

	/**
	 * @return array<string, mixed>|null
	 */
	public static function get_template_by_id( int $id ): ?array {
		if ( $id <= 0 ) {
			return null;
		}
		global $wpdb;
		$row = $wpdb->get_row(
			$wpdb->prepare( 'SELECT * FROM %i WHERE id = %d LIMIT 1', self::templates_table(), $id ),
			ARRAY_A
		);
		if ( ! $row ) {
			return null;
		}
		return self::inflate_template_row( $row );
	}

	/**
	 * Template shown in the admin editor (saved row or built-in defaults).
	 *
	 * @return array<string, mixed>
	 */
	public static function get_template_for_editor(): array {
		$id = max( 0, (int) Sanitizer::get( 'id', 'int' ) );
		if ( $id > 0 ) {
			$row = self::get_template_by_id( $id );
			if ( $row ) {
				return $row;
			}
		}
		return self::get_default_template();
	}

	/**
	 * @param array<string, mixed> $row
	 * @return array<string, mixed>
	 */
	private static function inflate_template_row( array $row ): array {
		$decoded = json_decode( (string) ( $row['design_json'] ?? '' ), true );
		$row['design'] = self::merge_design( is_array( $decoded ) ? $decoded : array() );
		if ( null === $row['discount_percent'] || '' === $row['discount_percent'] ) {
			$row['discount_percent'] = null;
		} else {
			$row['discount_percent'] = (float) $row['discount_percent'];
		}
		return $row;
	}

	public static function get_default_template(): array {
		global $wpdb;
		$row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE is_default = 1 ORDER BY id DESC LIMIT 1', self::templates_table() ), ARRAY_A );
		if ( $row ) {
			return self::inflate_template_row( $row );
		}
		return array(
			'name'                    => __( 'Default cart recovery', 'suitemkt3k' ),
			'subject'                 => __( 'Your purchase was not fully processed', 'suitemkt3k' ),
			'discount_percent'        => null,
			'discount_validity_hours' => 24,
			'design'                  => self::default_design(),
		);
	}

	public static function save_template( array $data, int $id = 0 ): int {
		global $wpdb;

		if ( $id <= 0 ) {
			$existing = self::get_default_template();
			if ( ! empty( $existing['id'] ) ) {
				$id = (int) $existing['id'];
			}
		}

		$design_input = isset( $data['design'] ) && is_array( $data['design'] ) ? Sanitizer::recursive_text( $data['design'] ) : array();
		if ( isset( $data['design']['body'] ) ) {
			$design_input['body'] = Sanitizer::textarea( $data['design']['body'], 10000 );
		}
		$design = self::merge_design( $design_input );

		$has_attached_coupon = isset( $data['recovery_coupon_id'] ) && (int) $data['recovery_coupon_id'] > 0;
		$discount_raw        = $data['discount_percent'] ?? null;
		$discount_percent    = null;
		if ( ! $has_attached_coupon && null !== $discount_raw && '' !== $discount_raw ) {
			$parsed = Sanitizer::float( $discount_raw, 0, 99 );
			if ( $parsed > 0 ) {
				$discount_percent = $parsed;
			}
		}

		$payload = array(
			'name'                    => Sanitizer::text( $data['name'] ?? __( 'Cart recovery', 'suitemkt3k' ), 190 ),
			'subject'                 => Sanitizer::text( $data['subject'] ?? '', 255 ),
			'design_json'             => wp_json_encode( $design ),
			'discount_percent'        => $discount_percent,
			'discount_validity_hours' => ( null !== $discount_percent && isset( $data['discount_validity_hours'] ) && '' !== $data['discount_validity_hours'] )
				? Sanitizer::int( $data['discount_validity_hours'], 1, 720 )
				: null,
			'is_default'              => 1,
		);
		$wpdb->update( self::templates_table(), array( 'is_default' => 0 ), array( 'is_default' => 1 ) );
		if ( $id > 0 ) {
			$wpdb->update( self::templates_table(), $payload, array( 'id' => $id ) );
			return $id;
		}
		$wpdb->insert( self::templates_table(), $payload );
		return (int) $wpdb->insert_id;
	}

	public static function send_email( int $cart_id, array $template = array(), bool $is_test = false, ?string $override_recipient = null ): bool {
		$cart = self::get( $cart_id );
		if ( ! $cart && ! $is_test ) {
			return false;
		}
		if ( ! $template ) {
			$template = self::get_default_template();
		}
		$design = $template['design'] ?? array();

		$discount_html  = '';
		$coupon_code    = '';
		$discount_label = '';
		$validity_hours = (int) ( $template['discount_validity_hours'] ?? 24 );
		$validity_text  = (string) ( $design['validity_text'] ?? __( 'This discount is valid for {{hours}} hours after the email is sent.', 'suitemkt3k' ) );

		if ( ! $is_test ) {
			$template_percent = isset( $template['discount_percent'] ) ? (float) $template['discount_percent'] : 0.0;

			// 1) Template discount % (exclusive — ignores the attached coupon in settings).
			if ( $template_percent > 0 ) {
				$coupon_code = self::create_recovery_coupon( $cart, $template_percent, $validity_hours );
				if ( $coupon_code ) {
					$discount_label = ( (int) $template_percent ) . '%';
					$discount_html  = sprintf(
						/* translators: %1$s coupon code, %2$s discount label */
						esc_html__( 'Use code %1$s to get %2$s off.', 'suitemkt3k' ),
						'<strong>' . esc_html( $coupon_code ) . '</strong>',
						esc_html( $discount_label )
					);
					if ( $validity_text ) {
						$discount_html .= '<br><span style="font-size:12px;color:#475569;">' . esc_html( strtr( $validity_text, array(
							'{{hours}}'  => (string) $validity_hours,
							'{{code}}'   => $coupon_code,
							'{{amount}}' => $discount_label,
						) ) ) . '</span>';
					}
				}
			} else {
				// 2) Attached coupon from the template editor (stored in cart settings).
				$cart_settings   = (array) Settings_Manager::get( 'cart', '', array() );
				$attached_coupon = isset( $cart_settings['recovery_coupon_id'] ) ? (int) $cart_settings['recovery_coupon_id'] : 0;
				if ( $attached_coupon ) {
					$coupon_row = \SuiteMkt3k\Modules\Coupons\Coupon_Manager::get( $attached_coupon );
					if ( $coupon_row && WooCommerce_Check::is_active() && \SuiteMkt3k\Modules\Coupons\Coupon_Manager::STATUS_ACTIVE === (string) ( $coupon_row['status'] ?? '' ) ) {
						$wc_post = get_post( (int) $coupon_row['wc_coupon_id'] );
						if ( $wc_post ) {
							$coupon_code    = (string) $wc_post->post_title;
							$discount_label = 'percent' === $coupon_row['discount_type'] ? ( $coupon_row['discount_value'] . '%' ) : ( $coupon_row['discount_value'] );
							$discount_html  = sprintf(
								/* translators: %1$s coupon code, %2$s discount label */
								esc_html__( 'Use code %1$s to get %2$s off.', 'suitemkt3k' ),
								'<strong>' . esc_html( $coupon_code ) . '</strong>',
								esc_html( $discount_label )
							);
						}
					}
				}
			}
		}

		$body = (string) ( $design['body'] ?? '' );
		$company_name = (string) get_bloginfo( 'name' );
		$body = strtr(
			$body,
			array(
				'{{name}}'           => $cart['name'] ?? __( 'there', 'suitemkt3k' ),
				'{{first_name}}'     => self::first_name( $cart['name'] ?? '' ),
				'{{company_name}}'   => $company_name,
				'{{nombre empresa}}' => $company_name,
			)
		);

		$cta_url = self::recovery_url( $cart );
		$unsub_url  = '';
		$unsub_text = __( 'You no longer want to receive emails like this?', 'suitemkt3k' );
		$unsub_cta  = __( 'Unsubscribe', 'suitemkt3k' );

		if ( ! $is_test && $cart ) {
			$subscriber = Subscriber_Manager::upsert(
				array(
					'name'                 => (string) ( $cart['name'] ?? '' ),
					'email'                => (string) ( $cart['email'] ?? '' ),
					'birth_date'           => '',
					'source_type'          => 'cart_recovery',
					'source_campaign_id'   => (int) ( $cart['id'] ?? 0 ),
					'source_campaign_name' => (string) ( $template['name'] ?? __( 'Abandoned cart', 'suitemkt3k' ) ),
					'ip'                   => Sanitizer::server( 'REMOTE_ADDR', 'text' ),
					'locale'               => get_locale(),
				)
			);
			if ( $subscriber ) {
				$unsub_url = Subscriber_Manager::unsubscribe_url( (string) ( $subscriber['unsubscribe_token'] ?? '' ) );
			}
		}

		$html    = Email_Renderer::render(
			'cart',
			array(
				'heading'          => $design['heading'] ?? '',
				'intro_line'       => $design['preheader'] ?? '',
				'body'             => $body,
				'cta_label'        => $design['cta_label'] ?? __( 'Resume my order', 'suitemkt3k' ),
				'cta_url'          => $cta_url,
				'image_url'        => $design['image_url'] ?? '',
				'accent'           => $design['accent'] ?? '#2AB2E5',
				'discount_html'    => $discount_html,
				'unsubscribe_url'  => $unsub_url,
				'unsubscribe_text' => (string) ( $design['unsubscribe_text'] ?? $unsub_text ),
				'unsubscribe_cta'  => (string) ( $design['unsubscribe_cta'] ?? $unsub_cta ),
				'cta_radius'       => (int) ( $design['cta_radius'] ?? 10 ),
			)
		);

		$to = $override_recipient ? Sanitizer::email( $override_recipient ) : (string) $cart['email'];
		if ( '' === $to ) {
			return false;
		}
		$mailer = new Mailer();
		$tracking_hash = $is_test ? '' : hash( 'sha256', 'cart|' . (string) $cart_id . '|' . microtime( true ) . '|' . wp_rand() . '|' . get_option( 'suitemkt3k_secret_key' ) );
		$ok = $mailer->send(
			array(
				'to'              => $to,
				'subject'         => $is_test ? '[TEST] ' . ( $template['subject'] ?? '' ) : (string) $template['subject'],
				'html'            => $html,
				'context'         => $is_test ? 'cart_test' : 'cart_recovery',
				'entity_id'       => $cart_id,
				'tracking_hash'   => $tracking_hash,
				'unsubscribe_url' => $unsub_url,
			)
		);
		if ( $ok && ! $is_test && $cart ) {
			global $wpdb;
			$update  = array(
				'recovery_email_sent_at' => current_time( 'mysql', true ),
				'status'                 => 'emailed',
			);
			$formats = array( '%s', '%s' );
			if ( $coupon_code ) {
				$update['recovery_coupon_code'] = $coupon_code;
				$formats[]                      = '%s';
			}
			$wpdb->update(
				self::table(),
				$update,
				array( 'id' => (int) $cart['id'] ),
				$formats,
				array( '%d' )
			);
		}
		return $ok;
	}

	private static function first_name( string $name ): string {
		$parts = preg_split( '/\s+/', trim( $name ) );
		return $parts ? (string) $parts[0] : '';
	}

	public static function recovery_url( ?array $cart ): string {
		if ( ! $cart || empty( $cart['recovery_token'] ) ) {
			return WooCommerce_Check::checkout_url();
		}
		return add_query_arg(
			array(
				'wmk_recover' => $cart['recovery_token'],
			),
			WooCommerce_Check::checkout_url()
		);
	}

	public static function maybe_handle_recovery_link(): void {
		$token = Sanitizer::get( 'wmk_recover', 'text' );
		if ( '' === $token || ! WooCommerce_Check::is_active() ) {
			return;
		}
		if ( strlen( $token ) !== 64 || ! ctype_xdigit( $token ) ) {
			return;
		}
		if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
			return;
		}

		global $wpdb;
		$row = $wpdb->get_row(
			$wpdb->prepare( 'SELECT * FROM %i WHERE recovery_token = %s', self::table(), $token ),
			ARRAY_A
		);
		if ( ! $row || empty( $row['cart_items_json'] ) ) {
			return;
		}
		$items = json_decode( (string) $row['cart_items_json'], true );
		if ( ! is_array( $items ) ) {
			return;
		}

		WC()->cart->empty_cart();
		foreach ( $items as $item ) {
			WC()->cart->add_to_cart(
				(int) ( $item['product_id'] ?? 0 ),
				max( 1, (int) ( $item['qty'] ?? 1 ) ),
				(int) ( $item['variation_id'] ?? 0 )
			);
		}

		self::prime_checkout_customer( $row );
		self::flag_recovery_session( (int) $row['id'] );

		$coupon_code = (string) ( $row['recovery_coupon_code'] ?? '' );
		if ( $coupon_code ) {
			WC()->cart->apply_coupon( $coupon_code );
		}

		wp_safe_redirect( WooCommerce_Check::checkout_url() );
		exit;
	}

	/**
	 * Remember that this shopper arrived via a recovery email link.
	 */
	public static function flag_recovery_session( int $cart_id ): void {
		if ( $cart_id <= 0 ) {
			return;
		}
		if ( function_exists( 'WC' ) && WC()->session ) {
			WC()->session->set( self::SESSION_RECOVERY_CART_ID, $cart_id );
			if ( method_exists( WC()->session, 'save_data' ) ) {
				WC()->session->save_data();
			}
		}
		// Cookie backup when the WC session is regenerated between click and checkout.
		if ( function_exists( 'wc_setcookie' ) ) {
			wc_setcookie( 'suitemkt3k_recovery_cart', (string) $cart_id, time() + WEEK_IN_SECONDS );
		} elseif ( ! headers_sent() ) {
			setcookie( 'suitemkt3k_recovery_cart', (string) $cart_id, time() + WEEK_IN_SECONDS, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, is_ssl(), true );
		}
	}

	/**
	 * Copy recovery attribution from session/cookie onto the new order.
	 *
	 * @param \WC_Order|mixed $order Order being created.
	 * @param array           $data  Checkout posted data (unused).
	 */
	public static function attach_recovery_meta_to_order( $order, $data = array() ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
		if ( ! is_object( $order ) || ! method_exists( $order, 'update_meta_data' ) ) {
			return;
		}
		$cart_id = self::session_recovery_cart_id();
		if ( $cart_id <= 0 ) {
			return;
		}
		$order->update_meta_data( self::ORDER_META_RECOVERY_CART_ID, $cart_id );
	}

	private static function session_recovery_cart_id(): int {
		if ( function_exists( 'WC' ) && WC()->session ) {
			$id = (int) WC()->session->get( self::SESSION_RECOVERY_CART_ID );
			if ( $id > 0 ) {
				return $id;
			}
		}
		if ( isset( $_COOKIE['suitemkt3k_recovery_cart'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			return max( 0, (int) Sanitizer::int( wp_unslash( $_COOKIE['suitemkt3k_recovery_cart'] ), 0 ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		}
		return 0;
	}

	private static function clear_recovery_session(): void {
		if ( function_exists( 'WC' ) && WC()->session ) {
			WC()->session->set( self::SESSION_RECOVERY_CART_ID, null );
			if ( method_exists( WC()->session, 'save_data' ) ) {
				WC()->session->save_data();
			}
		}
		if ( function_exists( 'wc_setcookie' ) ) {
			wc_setcookie( 'suitemkt3k_recovery_cart', '', time() - HOUR_IN_SECONDS );
		} elseif ( ! headers_sent() && isset( $_COOKIE['suitemkt3k_recovery_cart'] ) ) {
			setcookie( 'suitemkt3k_recovery_cart', '', time() - HOUR_IN_SECONDS, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, is_ssl(), true );
		}
	}

	private static function prime_checkout_customer( array $row ): void {
		if ( ! function_exists( 'WC' ) || ! WC()->customer ) {
			return;
		}
		$customer = WC()->customer;
		$billing  = array();
		if ( ! empty( $row['billing_json'] ) ) {
			$decoded = json_decode( (string) $row['billing_json'], true );
			if ( is_array( $decoded ) ) {
				$billing = $decoded;
			}
		}

		$name  = trim( (string) ( $row['name'] ?? '' ) );
		$parts = preg_split( '/\s+/', $name, 2 );
		$email = (string) ( $billing['billing_email'] ?? $row['email'] ?? '' );

		if ( $email ) {
			$customer->set_billing_email( $email );
			$customer->set_email( $email );
		}

		$map = array(
			'billing_first_name' => $billing['billing_first_name'] ?? ( $parts[0] ?? '' ),
			'billing_last_name'  => $billing['billing_last_name'] ?? ( $parts[1] ?? '' ),
			'billing_company'    => $billing['billing_company'] ?? '',
			'billing_address_1'  => $billing['billing_address_1'] ?? '',
			'billing_address_2'  => $billing['billing_address_2'] ?? '',
			'billing_city'       => $billing['billing_city'] ?? '',
			'billing_postcode'   => $billing['billing_postcode'] ?? '',
			'billing_country'    => $billing['billing_country'] ?? '',
			'billing_state'      => $billing['billing_state'] ?? '',
			'billing_phone'      => $billing['billing_phone'] ?? ( $row['phone'] ?? '' ),
		);
		foreach ( $map as $key => $value ) {
			if ( '' === (string) $value ) {
				continue;
			}
			$setter = 'set_' . $key;
			if ( method_exists( $customer, $setter ) ) {
				$customer->$setter( (string) $value );
			}
		}
		$customer->save();
	}

	/**
	 * Prevents stacking other coupons when a per-cart recovery coupon (template %) is active.
	 *
	 * @param bool            $valid  Whether the coupon is valid.
	 * @param \WC_Coupon|mixed $coupon Coupon object.
	 */
	public static function block_extra_coupons_with_recovery( $valid, $coupon ): bool {
		if ( ! $valid || ! function_exists( 'WC' ) || ! WC()->cart ) {
			return (bool) $valid;
		}
		$applied = WC()->cart->get_applied_coupons();
		if ( empty( $applied ) ) {
			return (bool) $valid;
		}
		$incoming = is_object( $coupon ) && method_exists( $coupon, 'get_code' ) ? (string) $coupon->get_code() : '';
		foreach ( $applied as $code ) {
			if ( $incoming && strcasecmp( $incoming, $code ) === 0 ) {
				return (bool) $valid;
			}
			$wc_coupon = new \WC_Coupon( $code );
			$cart_id   = (int) get_post_meta( $wc_coupon->get_id(), '_suitemkt3k_cart_id', true );
			if ( $cart_id > 0 ) {
				if ( $incoming && strcasecmp( $incoming, $code ) !== 0 ) {
					return false;
				}
			}
		}
		return (bool) $valid;
	}

	private static function resolve_attached_coupon_code(): string {
		$cart_settings   = (array) Settings_Manager::get( 'cart', '', array() );
		$attached_coupon = isset( $cart_settings['recovery_coupon_id'] ) ? (int) $cart_settings['recovery_coupon_id'] : 0;
		if ( ! $attached_coupon ) {
			return '';
		}
		$coupon_row = \SuiteMkt3k\Modules\Coupons\Coupon_Manager::get( $attached_coupon );
		if ( ! $coupon_row || empty( $coupon_row['wc_coupon_id'] ) ) {
			return '';
		}
		$wc_post = get_post( (int) $coupon_row['wc_coupon_id'] );
		return $wc_post ? (string) $wc_post->post_title : '';
	}

	private static function create_recovery_coupon( array $cart, float $percent, int $validity_hours ): string {
		if ( ! WooCommerce_Check::is_active() ) {
			return '';
		}
		$code = 'WMK-' . strtoupper( substr( bin2hex( random_bytes( 6 ) ), 0, 8 ) );
		$post_id = wp_insert_post(
			array(
				'post_title'  => $code,
				'post_status' => 'publish',
				'post_type'   => 'shop_coupon',
			)
		);
		if ( is_wp_error( $post_id ) || ! $post_id ) {
			return '';
		}
		update_post_meta( $post_id, 'discount_type', 'percent' );
		update_post_meta( $post_id, 'coupon_amount', (string) $percent );
		update_post_meta( $post_id, 'usage_limit', '1' );
		update_post_meta( $post_id, 'date_expires', (string) ( time() + max( 1, $validity_hours ) * HOUR_IN_SECONDS ) );
		update_post_meta( $post_id, 'individual_use', 'yes' );
		update_post_meta( $post_id, '_suitemkt3k_cart_id', $cart['id'] ?? 0 );
		return $code;
	}

	public static function handle_template_save(): void {
		Capabilities::ensure( Capabilities::EDIT_CAMPAIGN );
		check_admin_referer( Nonces::ADMIN_ACTION, '_wpnonce' );
		$id        = max( 0, (int) Sanitizer::post( 'id', 'int' ) );
		$coupon_id = max( 0, (int) Sanitizer::post( 'recovery_coupon_id', 'int' ) );
		$coupon_id = $coupon_id > 0 ? $coupon_id : null;
		$payload   = Sanitizer::json_post( 'payload' );
		if ( empty( $payload ) ) {
			$design_raw = isset( $_POST['design'] ) && is_array( $_POST['design'] ) ? wp_unslash( $_POST['design'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above
			$design     = Sanitizer::recursive_text( $design_raw );
			if ( isset( $design_raw['body'] ) ) {
				$design['body'] = Sanitizer::textarea( $design_raw['body'], 10000 );
			}
			$payload = array(
				'name'                    => Sanitizer::text( Sanitizer::post( 'name', 'text' ), 190 ),
				'subject'                 => Sanitizer::text( Sanitizer::post( 'subject', 'text' ), 255 ),
				'discount_percent'        => Sanitizer::post( 'discount_percent', 'text' ),
				'discount_validity_hours' => Sanitizer::post( 'discount_validity_hours', 'text' ),
				'is_default'              => true,
				'design'                  => $design,
			);
		} else {
			$payload['is_default'] = true;
		}
		if ( null !== $coupon_id ) {
			$payload['recovery_coupon_id'] = $coupon_id;
			if ( $coupon_id > 0 ) {
				$payload['discount_percent'] = '';
			}
		}
		$discount_for_settings = isset( $payload['discount_percent'] ) ? (float) $payload['discount_percent'] : 0.0;
		if ( $discount_for_settings > 0 ) {
			$payload['recovery_coupon_id'] = 0;
			$coupon_id                     = 0;
		}
		if ( null !== $coupon_id ) {
			$all  = (array) get_option( Settings_Manager::OPTION, array() );
			$cart = isset( $all['cart'] ) && is_array( $all['cart'] ) ? $all['cart'] : array();
			$cart['recovery_coupon_id'] = $coupon_id;
			$all['cart']                = $cart;
			update_option( Settings_Manager::OPTION, $all, false );
		}
		$id = self::save_template( $payload, $id );

		wp_safe_redirect( add_query_arg( array( 'view' => 'template', 'id' => $id, 'updated' => '1' ), admin_url( 'admin.php?page=suitemkt3k-cart-recovery' ) ) );
		exit;
	}

	public static function handle_send_email(): void {
		Capabilities::ensure( Capabilities::EDIT_CAMPAIGN );
		check_admin_referer( Nonces::ADMIN_ACTION, '_wpnonce' );
		$cart_id_raw = filter_input( INPUT_POST, 'cart_id', FILTER_SANITIZE_NUMBER_INT );
		$cart_id     = max( 0, (int) $cart_id_raw );
		if ( $cart_id ) {
			self::send_email( $cart_id );
		}
		wp_safe_redirect( add_query_arg( array( 'sent' => '1' ), admin_url( 'admin.php?page=suitemkt3k-cart-recovery' ) ) );
		exit;
	}

	public static function handle_test_email(): void {
		Capabilities::ensure( Capabilities::EDIT_CAMPAIGN );
		check_admin_referer( Nonces::ADMIN_ACTION, '_wpnonce' );
		$to       = Sanitizer::email( Sanitizer::post( 'test_email', 'email' ) );
		$template = Sanitizer::json_post( 'payload' );
		if ( empty( $template ) ) {
			$template = self::get_default_template();
		}
		$fake_cart = array(
			'id'              => 0,
			'name'            => __( 'Sample customer', 'suitemkt3k' ),
			'email'           => $to,
			'recovery_token'  => str_repeat( '0', 64 ),
		);
		$mailer = new Mailer();
		$design = $template['design'] ?? array();
		$company_name = (string) get_bloginfo( 'name' );
		$body   = strtr(
			(string) ( $design['body'] ?? '' ),
			array(
				'{{name}}'           => $fake_cart['name'],
				'{{first_name}}'     => $fake_cart['name'],
				'{{company_name}}'   => $company_name,
				'{{nombre empresa}}' => $company_name,
			)
		);
		$html   = Email_Renderer::render(
			'cart',
			array(
				'heading'          => $design['heading'] ?? '',
				'intro_line'       => $design['preheader'] ?? '',
				'body'             => $body,
				'cta_label'        => $design['cta_label'] ?? __( 'Resume my order', 'suitemkt3k' ),
				'cta_url'          => home_url( '/cart/' ),
				'accent'           => $design['accent'] ?? '#2AB2E5',
				'cta_radius'       => (int) ( $design['cta_radius'] ?? 10 ),
				'unsubscribe_text' => (string) ( $design['unsubscribe_text'] ?? '' ),
				'unsubscribe_cta'  => (string) ( $design['unsubscribe_cta'] ?? __( 'Unsubscribe', 'suitemkt3k' ) ),
				/* translators: %s: sample discount percentage. */
				'discount_html' => ! empty( $template['discount_percent'] ) ? sprintf( esc_html__( 'Sample discount: %s%%', 'suitemkt3k' ), (int) $template['discount_percent'] ) : '',
			)
		);
		$ok = $mailer->send( array(
			'to' => $to,
			'subject' => '[TEST] ' . ( $template['subject'] ?? '' ),
			'html' => $html,
			'context' => 'cart_test',
		) );
		wp_safe_redirect( add_query_arg( array( 'view' => 'template', 'test' => $ok ? 'sent' : 'failed' ), admin_url( 'admin.php?page=suitemkt3k-cart-recovery' ) ) );
		exit;
	}

	public static function handle_delete(): void {
		Capabilities::ensure( Capabilities::EDIT_CAMPAIGN );
		check_admin_referer( Nonces::ADMIN_ACTION, '_wpnonce' );
		$id_raw = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT );
		$id     = max( 0, (int) $id_raw );
		if ( $id ) {
			global $wpdb;
			$wpdb->update( self::table(), array( 'status' => 'deleted' ), array( 'id' => $id ), array( '%s' ), array( '%d' ) );
		}
		wp_safe_redirect( admin_url( 'admin.php?page=suitemkt3k-cart-recovery&deleted=1' ) );
		exit;
	}

	public static function handle_delete_all(): void {
		Capabilities::ensure( Capabilities::EDIT_CAMPAIGN );
		check_admin_referer( Nonces::ADMIN_ACTION, '_wpnonce' );
		global $wpdb;
		$wpdb->query( $wpdb->prepare( 'TRUNCATE TABLE %i', self::table() ) );
		Logger::warning( 'cart_recovery', 'Abandoned carts table truncated by admin' );
		wp_safe_redirect( admin_url( 'admin.php?page=suitemkt3k-cart-recovery&deleted_all=1' ) );
		exit;
	}

	/**
	 * Import WooCommerce orders with status "cancelled" or "draft" that own a
	 * customer email as an abandoned cart record. Already-imported orders or
	 * orders older than the configured purge horizon are skipped.
	 */
	public static function handle_import_wc(): void {
		Capabilities::ensure( Capabilities::EDIT_CAMPAIGN );
		check_admin_referer( Nonces::ADMIN_ACTION, '_wpnonce' );

		$imported = 0;
		if ( WooCommerce_Check::is_active() && function_exists( 'wc_get_orders' ) ) {
			$settings  = (array) Settings_Manager::get( 'cart', '', array() );
			$purge_days = isset( $settings['purge_days'] ) ? max( 1, (int) $settings['purge_days'] ) : 30;
			$threshold  = gmdate( 'Y-m-d H:i:s', time() - $purge_days * DAY_IN_SECONDS );

			$orders = wc_get_orders(
				array(
					'status'       => array( 'cancelled', 'draft', 'checkout-draft' ),
					'limit'        => 200,
					'orderby'      => 'date',
					'order'        => 'DESC',
					'date_created' => '>=' . gmdate( 'Y-m-d', time() - $purge_days * DAY_IN_SECONDS ),
				)
			);

			global $wpdb;
			foreach ( (array) $orders as $order ) {
				if ( ! $order instanceof \WC_Order ) {
					continue;
				}
				$email = (string) $order->get_billing_email();
				if ( ! $email ) {
					continue;
				}
				$created_gmt = $order->get_date_created() ? $order->get_date_created()->date( 'Y-m-d H:i:s' ) : current_time( 'mysql', true );
				if ( $created_gmt < $threshold ) {
					continue;
				}
				$exists = (int) $wpdb->get_var(
					$wpdb->prepare(
						'SELECT COUNT(*) FROM %i WHERE wc_order_id = %d OR (email = %s AND status != %s AND last_activity_at >= %s)',
						self::table(),
						$order->get_id(),
						$email,
						'deleted',
						$threshold
					)
				);
				if ( $exists ) {
					continue;
				}

				$items = array();
				foreach ( $order->get_items() as $item ) {
					if ( $item instanceof \WC_Order_Item_Product ) {
						$items[] = array(
							'product_id'   => $item->get_product_id(),
							'variation_id' => $item->get_variation_id(),
							'qty'          => $item->get_quantity(),
							'name'         => $item->get_name(),
						);
					}
				}

				$token = bin2hex( random_bytes( 32 ) );
				$wpdb->insert(
					self::table(),
					array(
						'wc_order_id'      => $order->get_id(),
						'email'            => $email,
						'name'             => trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ),
						'phone'            => (string) $order->get_billing_phone(),
						'cart_items_json'  => wp_json_encode( $items ),
						'total'            => (float) $order->get_total(),
						'currency'         => (string) $order->get_currency(),
						'status'           => 'pending',
						'recovery_token'   => $token,
						'last_activity_at' => $created_gmt,
						'created_at'       => current_time( 'mysql', true ),
					)
				);
				if ( $wpdb->insert_id ) {
					$imported++;
				}
			}
		}

		wp_safe_redirect(
			add_query_arg(
				array( 'view' => 'list', 'imported' => $imported ),
				admin_url( 'admin.php?page=suitemkt3k-cart-recovery' )
			)
		);
		exit;
	}

	/**
	 * Mark a cart as recovered only when the order is attributed to a recovery email click
	 * (session / order meta / recovery coupon). Plain checkouts with the same email are not recoveries.
	 */
	public static function maybe_mark_recovered( int $order_id ): void {
		if ( ! WooCommerce_Check::is_active() || ! function_exists( 'wc_get_order' ) ) {
			return;
		}
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return;
		}

		$cart_id = self::resolve_recovery_attribution( $order );
		if ( $cart_id > 0 ) {
			self::mark_cart_recovered( $cart_id, $order_id, $order );
		}

		$email = (string) $order->get_billing_email();
		if ( $email ) {
			self::close_open_carts_for_email( $email, $order_id, $cart_id );
		}

		self::clear_recovery_session();
	}

	/**
	 * Resolve the abandoned-cart ID that this order recovered, if any.
	 *
	 * @param \WC_Order $order Order.
	 */
	private static function resolve_recovery_attribution( $order ): int {
		$candidates = array();

		$meta_id = (int) $order->get_meta( self::ORDER_META_RECOVERY_CART_ID );
		if ( $meta_id > 0 ) {
			$candidates[] = $meta_id;
		}

		$session_id = self::session_recovery_cart_id();
		if ( $session_id > 0 ) {
			$candidates[] = $session_id;
		}

		if ( method_exists( $order, 'get_coupon_codes' ) ) {
			foreach ( (array) $order->get_coupon_codes() as $code ) {
				$wc_coupon = new \WC_Coupon( $code );
				$from_coupon = (int) get_post_meta( $wc_coupon->get_id(), '_suitemkt3k_cart_id', true );
				if ( $from_coupon > 0 ) {
					$candidates[] = $from_coupon;
				}
			}
		}

		$candidates = array_values( array_unique( array_filter( array_map( 'intval', $candidates ) ) ) );
		if ( empty( $candidates ) ) {
			return 0;
		}

		global $wpdb;
		foreach ( $candidates as $id ) {
			$row = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT id, status, recovery_email_sent_at, recovered_order_id
					FROM %i WHERE id = %d LIMIT 1",
					self::table(),
					$id
				),
				ARRAY_A
			);
			if ( ! $row ) {
				continue;
			}
			// Already recovered for this order — treat as attributed (idempotent).
			if ( 'recovered' === ( $row['status'] ?? '' ) && (int) ( $row['recovered_order_id'] ?? 0 ) === (int) $order->get_id() ) {
				return (int) $row['id'];
			}
			// Must have received a recovery email; pending capture-only carts do not count.
			$emailed = ! empty( $row['recovery_email_sent_at'] ) || 'emailed' === ( $row['status'] ?? '' );
			if ( ! $emailed ) {
				continue;
			}
			if ( in_array( (string) ( $row['status'] ?? '' ), array( 'recovered', 'deleted', 'completed', 'expired' ), true )
				&& (int) ( $row['recovered_order_id'] ?? 0 ) !== (int) $order->get_id() ) {
				continue;
			}
			return (int) $row['id'];
		}

		return 0;
	}

	/**
	 * @param \WC_Order $order Order.
	 */
	private static function mark_cart_recovered( int $cart_id, int $order_id, $order ): void {
		global $wpdb;
		$current = $wpdb->get_var(
			$wpdb->prepare( 'SELECT status FROM %i WHERE id = %d', self::table(), $cart_id )
		);
		if ( 'recovered' === $current ) {
			$existing_order = (int) $wpdb->get_var(
				$wpdb->prepare( 'SELECT recovered_order_id FROM %i WHERE id = %d', self::table(), $cart_id )
			);
			if ( $existing_order === $order_id ) {
				return;
			}
		}

		$updated = $wpdb->update(
			self::table(),
			array(
				'status'             => 'recovered',
				'recovered_at'       => current_time( 'mysql', true ),
				'recovered_order_id' => $order_id,
			),
			array( 'id' => $cart_id ),
			array( '%s', '%s', '%d' ),
			array( '%d' )
		);
		if ( false === $updated ) {
			return;
		}

		Subscriber_Manager::upsert(
			array(
				'name'                 => (string) $order->get_formatted_billing_full_name(),
				'email'                => (string) $order->get_billing_email(),
				'birth_date'           => '',
				'source_type'          => 'cart_recovery',
				'source_campaign_id'   => $cart_id,
				'source_campaign_name' => __( 'Recovered cart', 'suitemkt3k' ),
				'ip'                   => Sanitizer::server( 'REMOTE_ADDR', 'text' ),
				'locale'               => get_locale(),
			)
		);

		if ( class_exists( '\SuiteMkt3k\Modules\Statistics\Stats_Collector' ) ) {
			\SuiteMkt3k\Modules\Statistics\Stats_Collector::record(
				array(
					'event_type'  => 'conversion',
					'entity_type' => 'cart',
					'entity_id'   => $cart_id,
					'meta'        => array( 'order_id' => $order_id ),
				)
			);
		}

		Logger::info( 'cart_recovery', 'Cart recovered via recovery link', array( 'cart_id' => $cart_id, 'order_id' => $order_id ) );
	}

	/**
	 * Close leftover pending/emailed carts for this email after a purchase so they
	 * are not emailed again and are not counted as recovered.
	 */
	public static function close_open_carts_for_email( string $email, int $order_id, int $except_cart_id = 0 ): void {
		$email = Sanitizer::email( $email );
		if ( '' === $email ) {
			return;
		}
		global $wpdb;
		$sql = $wpdb->prepare(
			"UPDATE %i
			SET status = %s, wc_order_id = COALESCE(wc_order_id, %d)
			WHERE email = %s
			AND status IN ('pending','emailed')",
			self::table(),
			'completed',
			$order_id,
			$email
		);
		if ( $except_cart_id > 0 ) {
			$sql .= $wpdb->prepare( ' AND id != %d', $except_cart_id );
		}
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- built with prepare above
	}
}
