<?php
/**
 * Checkout Event Subscriber for Cart Recovery
 *
 * Handles WordPress events related to checkout, purchase, and user login.
 *
 * @package EDD\Pro\CartRecovery\Events
 * @copyright Copyright (c) 2026, Sandhills Development, LLC
 * @license https://opensource.org/licenses/GPL-2.0 GNU Public License
 * @since   3.6.5
 */

declare(strict_types=1);

namespace EDD\Pro\CartRecovery\Events;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore

use EDD\EventManagement\SubscriberInterface;
use EDD\Pro\CartRecovery\Cron\SingleEventScheduler;
use EDD\Pro\CartRecovery\Snapshots\SnapshotPersistence;
use EDD\Pro\CartRecovery\Carts\Repository;
use EDD\Pro\CartRecovery\Database\Queries\RecoveryCart;
use EDD\Pro\CartRecovery\Journey\EventLogger;

/**
 * Checkout Event Subscriber class.
 *
 * Subscribes to WordPress events related to checkout process and purchase completion.
 */
class CheckoutEventSubscriber implements SubscriberInterface {

	/**
	 * Snapshot persistence instance.
	 *
	 * @var SnapshotPersistence
	 */
	private SnapshotPersistence $snapshot_persistence;

	/**
	 * Event logger instance.
	 *
	 * @var EventLogger
	 */
	private EventLogger $event_logger;

	/**
	 * Constructor.
	 *
	 * @param SnapshotPersistence $snapshot_persistence Snapshot persistence instance.
	 * @param EventLogger|null    $event_logger         Event logger instance.
	 */
	public function __construct( SnapshotPersistence $snapshot_persistence, ?EventLogger $event_logger = null ) {
		$this->snapshot_persistence = $snapshot_persistence;
		$this->event_logger         = $event_logger ?? new EventLogger();
	}

	/**
	 * Returns subscribed events.
	 *
	 * @since 3.6.5
	 * @return array Event subscriptions.
	 */
	public static function get_subscribed_events(): array {
		return array(
			'edd_pre_process_purchase' => array( 'on_checkout_snapshot', 20 ),
			'edd_complete_purchase'    => array( 'on_purchase_completed', 100, 1 ),
			'wp_login'                 => array( 'on_user_login', 20, 2 ),
		);
	}

	/**
	 * Captures a snapshot right before checkout processing.
	 *
	 * @since 3.6.5
	 * @return void
	 */
	public function on_checkout_snapshot(): void {
		$this->snapshot_persistence->persist_snapshot();
	}

	/**
	 * Marks carts recovered on completed purchase, or deletes if never abandoned.
	 *
	 * @since 3.6.5
	 * @param int $order_id Order ID.
	 * @return void
	 */
	public function on_purchase_completed( int $order_id ): void {

		// Handle the current session's cart first.
		if ( $this->was_cart_abandoned() ) {
			$this->recover_cart_from_session( $order_id );
		} elseif ( ! empty( EDD()->session->get( 'acr_cart' ) ) ) {
			// Session has acr_cart but cart was never abandoned — just clean up.
			$this->maybe_delete_cart();
		}

		// DB fallback: find any remaining active cart for this order's customer.
		// This catches orphaned carts from previous sessions/checkout attempts.
		$cart = $this->find_active_cart_for_order( $order_id );
		if ( ! $cart ) {
			return;
		}

		$this->cleanup_orphaned_cart( $cart, $order_id );
	}

	/**
	 * Recovers an abandoned cart using session data (fast path).
	 *
	 * @since 3.6.5
	 *
	 * @param int $order_id Order ID.
	 * @return void
	 */
	private function recover_cart_from_session( int $order_id ): void {
		$cart_id = (int) EDD()->session->get( 'acr_cart' );

		// Always cancel scheduled recovery emails first, regardless of status update success.
		// This is critical to prevent sending emails after purchase completion.
		if ( ! empty( $cart_id ) ) {
			SingleEventScheduler::cancel_recovery_email( $cart_id );
		}

		// Cart was abandoned, so mark it as recovered to track the recovery.
		$persisted = $this->snapshot_persistence->persist_snapshot(
			array(
				'status'         => 'recovered',
				'date_recovered' => edd_get_utc_date_string(),
				'order_id'       => $order_id,
			)
		);

		// If status update failed, clean up session and return.
		if ( empty( $persisted['cart_id'] ) ) {
			EDD()->session->set( 'acr_email', null );
			EDD()->session->set( 'acr_cart', null );
			return;
		}

		// Get the email ID that was clicked (if any) from session.
		$converting_email_id = (int) EDD()->session->get( 'acr_email' );

		// Mark the recovery email that led to conversion as converted and used.
		$this->mark_recovery_email_converted( $cart_id, $converting_email_id );

		// Log cart_converted journey event.
		$order      = edd_get_order( $order_id );
		$event_data = array(
			'order_id'    => $order_id,
			'order_total' => $order ? $order->total : 0,
		);
		$this->event_logger->log_cart_event( 'cart_converted', $cart_id, $event_data );

		// Log email_converted for the specific email that converted (if any).
		$this->log_email_conversion( $cart_id, $converting_email_id );

		// Clean up session data.
		EDD()->session->set( 'acr_email', null );
		EDD()->session->set( 'acr_cart', null );
	}

	/**
	 * Marks the specific recovery email that led to conversion.
	 *
	 * When a purchase is completed, we mark only the recovery email that was clicked
	 * as "converted" and set date_used so the token is no longer valid.
	 *
	 * If no email ID is provided (direct purchase without clicking email), no emails are marked.
	 *
	 * @since 3.6.5
	 * @param int      $cart_id  Cart ID.
	 * @param int|null $email_id Email ID that led to conversion, or null.
	 * @return void
	 */
	private function mark_recovery_email_converted( int $cart_id, ?int $email_id ): void {
		if ( empty( $email_id ) ) {
			return;
		}

		global $wpdb;

		$now = edd_get_utc_date_string();

		$wpdb->update(
			"{$wpdb->prefix}edd_acr_emails",
			array(
				'delivery_status' => 'converted',
				'date_converted'  => $now,
			),
			array(
				'cart_id' => $cart_id,
				'id'      => $email_id,
			),
			array( '%s', '%s' ),
			array( '%d', '%d' )
		);
	}

	/**
	 * Log email_converted event for the specific email that led to conversion.
	 *
	 * @since 3.6.5
	 * @param int      $cart_id  Cart ID.
	 * @param int|null $email_id Email ID that led to conversion, or null.
	 * @return void
	 */
	private function log_email_conversion( int $cart_id, ?int $email_id ): void {
		if ( empty( $email_id ) ) {
			return;
		}

		$this->event_logger->log_email_event(
			'email_converted',
			$cart_id,
			$email_id
		);
	}

	/**
	 * Deletes the cart for the current session if it exists.
	 *
	 * Used when a purchase is completed and the cart was never abandoned.
	 *
	 * @since 3.6.5
	 * @return void
	 */
	private function maybe_delete_cart(): void {

		// Query for existing cart.
		$cart_id = EDD()->session->get( 'acr_cart' );
		if ( empty( $cart_id ) ) {
			return;
		}

		$query = new RecoveryCart();
		$cart  = $query->get_item( $cart_id );
		if ( ! $cart || empty( $cart->id ) ) {
			return;
		}

		// Delete the cart and its related recovery emails.
		$repository = new \EDD\Pro\CartRecovery\Carts\Repository();
		$repository->delete_cart( (int) $cart->id );
		EDD()->session->set( 'acr_cart', null );
	}

	/**
	 * Checks if the current cart was abandoned before completion.
	 *
	 * @since 3.6.5
	 * @return bool True if cart was abandoned, false otherwise.
	 */
	private function was_cart_abandoned(): bool {
		$cart_id = EDD()->session->get( 'acr_cart' );
		if ( empty( $cart_id ) ) {
			return false;
		}

		$query = new RecoveryCart();
		$cart  = $query->get_item( $cart_id );
		if ( ! $cart || empty( $cart->id ) ) {
			return false;
		}

		return ! empty( $cart->date_abandoned );
	}

	/**
	 * Finds any active (non-terminal) cart associated with an order's customer.
	 *
	 * Unlike the previous find_abandoned_cart_for_order(), this also matches
	 * 'tracking' carts that haven't yet been marked abandoned by the detection cron.
	 * This prevents orphaned carts from previous sessions triggering recovery emails.
	 *
	 * @since 3.6.7
	 *
	 * @param int $order_id Order ID.
	 * @return object|null Cart object if an active cart is found, null otherwise.
	 */
	private function find_active_cart_for_order( int $order_id ) {
		$order = edd_get_order( $order_id );
		if ( ! $order ) {
			return null;
		}

		$attributes = array_filter(
			array(
				'email'       => $order->email ?? '',
				'customer_id' => (int) ( $order->customer_id ?? 0 ),
			)
		);

		if ( empty( $attributes ) ) {
			return null;
		}

		$repository = new Repository();
		$cart       = $repository->find_primary_cart( $attributes );

		if ( ! $cart || empty( $cart->id ) ) {
			return null;
		}

		return $cart;
	}

	/**
	 * Cleans up an orphaned cart found via DB fallback after purchase completion.
	 *
	 * Abandoned carts are marked as recovered (with order linkage) to preserve analytics.
	 * Tracking carts are deleted since they were never abandoned.
	 *
	 * @since 3.6.7
	 *
	 * @param object $cart     Cart object from DB.
	 * @param int    $order_id Completed order ID.
	 * @return void
	 */
	private function cleanup_orphaned_cart( object $cart, int $order_id ): void {
		$cart_id = (int) $cart->id;
		$status  = $cart->status ?? '';

		// Cancel any scheduled recovery emails regardless of status.
		SingleEventScheduler::cancel_recovery_email( $cart_id );

		if ( 'abandoned' === $status ) {
			// Abandoned cart: mark as recovered to preserve analytics.
			$query = new RecoveryCart();
			$query->update_item(
				$cart_id,
				array(
					'status'         => 'recovered',
					'date_recovered' => edd_get_utc_date_string(),
					'order_id'       => $order_id,
				)
			);

			// Log cart_converted journey event.
			$order      = edd_get_order( $order_id );
			$event_data = array(
				'order_id'    => $order_id,
				'order_total' => $order ? $order->total : 0,
			);
			$this->event_logger->log_cart_event( 'cart_converted', $cart_id, $event_data );
		} else {
			// Tracking or unsubscribed: delete since never truly abandoned.
			$repository = new Repository();
			$repository->delete_cart( $cart_id );
		}
	}

	/**
	 * When a user logs in we re-key the cart with their customer identity.
	 *
	 * @since 3.6.5
	 * @param string   $user_login Username.
	 * @param \WP_User $user       User object.
	 * @return void
	 */
	public function on_user_login( string $user_login, \WP_User $user ): void {
		if ( ! ( $user instanceof \WP_User ) ) {
			return;
		}

		// Only persist if the cart has items — don't create empty cart rows for logins.
		if ( ! edd_get_cart_quantity() ) {
			return;
		}

		$identifiers = array( 'email' => sanitize_email( $user->user_email ) );

		$customer = edd_get_customer_by( 'user_id', $user->ID );
		if ( $customer instanceof \EDD_Customer ) {
			$identifiers['customer_id'] = (int) $customer->id;
		}

		$this->snapshot_persistence->persist_snapshot( $identifiers );
	}
}
