<?php
/**
 * Recovery cart restoration service.
 *
 * @package EDD\Pro\CartRecovery\Services
 * @copyright Copyright (c) 2026, Sandhills Development, LLC
 * @license https://opensource.org/licenses/GPL-2.0 GNU Public License
 * @since 3.6.5
 */

namespace EDD\Pro\CartRecovery\Services;

use EDD\Pro\CartRecovery\Database\Queries\RecoveryCart;
use EDD\Pro\CartRecovery\Database\Queries\RecoveryEmail;
use EDD\Pro\CartRecovery\Journey\EventLogger;
use EDD\Pro\CartRecovery\Tracking\Repository as TrackingRepository;
use WP_Error;

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

/**
 * Restores carts from recovery email tokens.
 *
 * @since 3.6.5
 */
final class Restore {

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

	/**
	 * Saved cart items with their original prices.
	 *
	 * @since 3.6.5
	 * @var array
	 */
	private $saved_items = array();

	/**
	 * Keys of items that were successfully restored to the cart.
	 *
	 * @since 3.6.5
	 * @var array
	 */
	private $restored_items = array();

	/**
	 * Constructor.
	 *
	 * @since 3.6.5
	 */
	public function __construct() {
		$this->event_logger = new EventLogger();
	}

	/**
	 * Restores the cart associated with a recovery token.
	 *
	 * @since 3.6.5
	 * @param string $token Recovery token hash.
	 * @return array|WP_Error Details about the restored cart or WP_Error on failure.
	 * @throws \RuntimeException If the cart cannot be updated.
	 */
	public function restore( string $token ) {
		$token = trim( $token );

		if ( empty( $token ) ) {
			edd_debug_log( '[EDD ACR] Cart restore failed: edd_acr_invalid_token', true );
			return new WP_Error( 'edd_acr_restore_failed', __( 'The recovery link is missing or invalid.', 'easy-digital-downloads' ) );
		}

		$email_query = new RecoveryEmail();
		$email       = $email_query->get_item_by( 'recovery_token', $token );
		if ( ! $email || empty( $email->id ) ) {
			edd_debug_log( '[EDD ACR] Cart restore failed: edd_acr_invalid_token' );
			return new WP_Error( 'edd_acr_restore_failed', __( 'This recovery link is no longer valid.', 'easy-digital-downloads' ) );
		}

		// Allow reusing the recovery link until the cart is actually purchased.
		// The token should only be invalidated when converted (purchased) or expired.
		if ( ! empty( $email->delivery_status ) && 'converted' === $email->delivery_status ) {
			edd_debug_log( '[EDD ACR] Cart restore skipped: cart already purchased', true );
			return new WP_Error( 'edd_acr_restore_failed', __( 'This cart has already been purchased.', 'easy-digital-downloads' ) );
		}

		if ( ! empty( $email->date_expired ) ) {
			$raw_expiry = (string) $email->date_expired;
			$utc_time   = \DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $raw_expiry, new \DateTimeZone( 'UTC' ) );
			if ( ! ( $utc_time instanceof \DateTimeImmutable ) ) {
				try {
					$utc_time = new \DateTimeImmutable( $raw_expiry, new \DateTimeZone( 'UTC' ) );
				} catch ( \Exception $exception ) {
					$utc_time = false;
				}
			}
			$expiry = $utc_time instanceof \DateTimeImmutable ? $utc_time->getTimestamp() : false;
			// Reject tokens that have passed their UTC expiry timestamp.
			if ( false !== $expiry && $expiry < time() ) {
				edd_debug_log( '[EDD ACR] Cart restore failed: edd_acr_token_expired', true );
				return new WP_Error( 'edd_acr_restore_failed', __( 'This recovery link has expired.', 'easy-digital-downloads' ) );
			}
		}

		$cart_query = new RecoveryCart();
		$cart       = $cart_query->get_item( (int) $email->cart_id );
		if ( ! $cart ) {
			edd_debug_log( '[EDD ACR] Cart restore failed: edd_acr_unknown_cart', true );
			return new WP_Error( 'edd_acr_restore_failed', __( 'We could not locate the original cart for this link.', 'easy-digital-downloads' ) );
		}

		if ( ! empty( $cart->date_unsubscribed ) ) {
			edd_debug_log( '[EDD ACR] Cart restore failed: edd_acr_unsubscribed', true );
			return new WP_Error( 'edd_acr_restore_failed', __( 'Email recovery is disabled for this address.', 'easy-digital-downloads' ) );
		}

		$items = $cart->get_items();
		if ( empty( $items ) ) {
			edd_debug_log( '[EDD ACR] Cart restore failed: edd_acr_empty_restore', true );
			return new WP_Error( 'edd_acr_restore_failed', __( 'There were no items to restore for this cart.', 'easy-digital-downloads' ) );
		}

		if ( ! $this->rebuild_cart( $items ) ) {
			edd_debug_log( '[EDD ACR] Cart restore failed: edd_acr_rebuild_failed', true );
			return new WP_Error( 'edd_acr_restore_failed', __( 'We were unable to rebuild your cart. Please add the items again.', 'easy-digital-downloads' ) );
		}

		$discount_code = $this->get_discount_code( $cart );
		if ( ! empty( $discount_code ) ) {
			edd_set_cart_discount( $discount_code );
			edd_debug_log( sprintf( '[EDD ACR] Applied discount code: %s', $discount_code ) );
		}

		// Check for price changes after discount has been applied.
		$this->check_price_changes();

		$cart_values  = array(
			'date_modified' => edd_get_utc_date_string(),
			'fail_count'    => 0,
		);
		$cart_formats = array( '%s', '%d' );

		global $wpdb;
		$wpdb->query( 'START TRANSACTION' );

		try {
			$cart_updated = $wpdb->update(
				$wpdb->prefix . 'edd_acr_carts',
				$cart_values,
				array( 'id' => (int) $cart->id ),
				$cart_formats,
				array( '%d' )
			);

			if ( false === $cart_updated ) {
				throw new \RuntimeException( 'Failed to update cart during restore transaction.' );
			}

			// Update the delivery status to track that the link was clicked.
			// Don't set date_used yet - that should only happen on actual purchase.
			$email_updated = $wpdb->update(
				$wpdb->prefix . 'edd_acr_emails',
				array(
					'delivery_status' => 'clicked',
				),
				array( 'id' => (int) $email->id ),
				array( '%s' ),
				array( '%d' )
			);

			if ( false === $email_updated ) {
				throw new \RuntimeException( 'Failed to update recovery email status during restore transaction.' );
			}

			$wpdb->query( 'COMMIT' );

		} catch ( \Throwable $e ) {
			$wpdb->query( 'ROLLBACK' );
			edd_debug_log( '[EDD ACR] Restore transaction failed: ' . $e->getMessage() );
			return new WP_Error( 'edd_acr_restore_failed', __( 'Could not complete the restore process. Please try again.', 'easy-digital-downloads' ) );
		}

		EDD()->session->set( 'acr_cart', (int) $cart->id );
		EDD()->session->set( 'acr_email', (int) $email->id );

		// Record the click using the tracking repository to ensure date_clicked is set
		// and the email_clicked journey event is logged.
		$tracking_repo = new TrackingRepository( $this->event_logger );
		$tracking_repo->record_click( (int) $email->id );

		edd_debug_log( sprintf( '[EDD ACR] Token clicked for cart %d: %s', (int) $cart->id, $token ) );

		// Determine which discount was applied for event logging.
		$applied_discount = $discount_code; // Use the already validated discount code from earlier.

		// Log cart_restored journey event.
		$event_data = array(
			'email_id'       => (int) $email->id,
			'recovery_token' => $token,
			'discount_code'  => $applied_discount,
		);
		$this->event_logger->log_cart_event( 'cart_restored', (int) $cart->id, $event_data );

		do_action( 'edd_acr_cart_restored', (int) $cart->id, (int) $email->id );

		// Pre-populate the email in session for a smoother checkout experience.
		if ( ! is_user_logged_in() ) {
			$customer_data = array(
				'email' => $cart->email,
			);
			$name          = $cart->get_customer_name();
			if ( ! empty( $name['first_name'] ) ) {
				$customer_data['first_name'] = $name['first_name'];
			}
			if ( ! empty( $name['last_name'] ) ) {
				$customer_data['last_name'] = $name['last_name'];
			}

			\EDD\Sessions\Customer::set( $customer_data );
		}

		edd_debug_log( sprintf( '[EDD ACR] Cart restored successfully for cart %d via email %d (token %s)', (int) $cart->id, (int) $email->id, $token ) );

		return array(
			'cart_id'  => (int) $cart->id,
			'email_id' => (int) $email->id,
		);
	}

	/**
	 * Populates the EDD cart with the provided items.
	 *
	 * @since 3.6.5
	 * @param array $items Cart items from edd_get_cart_content_details().
	 * @return bool Whether at least one item was added.
	 */
	private function rebuild_cart( array $items ): bool {
		// Reset tracking properties for this restore operation.
		$this->saved_items    = array();
		$this->restored_items = array();

		$current_contents   = edd_get_cart_contents();
		$has_existing_items = ! empty( $current_contents );

		if ( ! $has_existing_items ) {
			// Only wipe the cart when the shopper did not already add fresh items.
			edd_empty_cart();
		}

		$restored_any      = false;
		$unavailable_items = array();

		foreach ( $items as $item ) {
			// Extract fields from edd_get_cart_content_details() structure.
			$download_id = isset( $item['id'] ) ? absint( $item['id'] ) : 0;
			if ( ! $download_id ) {
				continue;
			}

			// Validate that the product exists and is published before adding to cart.
			$download = edd_get_download( $download_id );
			if ( ! $download ) {
				$unavailable_items[] = array(
					'id'    => $download_id,
					/* translators: %d: Product ID number */
					'title' => sprintf( __( 'Product #%d', 'easy-digital-downloads' ), $download_id ),
				);
				continue; // Skip non-existent products.
			}

			$product_title = edd_get_download_name( $download_id );

			// Check if the product can be purchased (handles stock and other availability checks).
			if ( ! $download->can_purchase() ) {
				$unavailable_items[] = array(
					'id'    => $download_id,
					'title' => $product_title,
				);
				continue; // Skip unavailable products (out of stock, etc.).
			}

			// Get options from the stored cart item - this preserves ALL data
			// including recurring details, upgrades, license renewals, etc.
			$item_number    = isset( $item['item_number'] ) && is_array( $item['item_number'] ) ? $item['item_number'] : array();
			$stored_options = isset( $item_number['options'] ) && is_array( $item_number['options'] ) ? $item_number['options'] : array();

			// Get quantity from stored data.
			$quantity = isset( $item['quantity'] ) ? absint( $item['quantity'] ) : 1;
			if ( isset( $item_number['quantity'] ) ) {
				$quantity = absint( $item_number['quantity'] );
			}
			$stored_options['quantity'] = $quantity;

			// Get price_id for the duplicate check and price tracking key.
			$price_id = isset( $stored_options['price_id'] ) && '' !== $stored_options['price_id'] ? (string) $stored_options['price_id'] : null;

			// Store the saved price for later comparison (after discount is applied).
			$stored_price = null;
			if ( isset( $item['price'] ) ) {
				$stored_price = (float) $item['price'];
			} elseif ( isset( $item['item_price'] ) ) {
				$stored_price = (float) $item['item_price'];
			}

			$key = $this->build_cart_line_key( $download_id, $price_id );

			// Track this saved item for later price comparison.
			if ( ! is_null( $stored_price ) ) {
				$this->saved_items[ $key ] = array(
					'download_id' => $download_id,
					'price_id'    => $price_id,
					'title'       => $product_title,
					'price'       => $stored_price,
				);
			}

			// Build options for the edd_item_in_cart() check.
			$cart_check_options = array();
			if ( ! empty( $item_number['hash'] ) ) {
				$cart_check_options['hash'] = $item_number['hash'];
			}
			if ( ! is_null( $price_id ) ) {
				$cart_check_options['price_id'] = $price_id;
			}

			// Skip items already in the cart to prevent duplicates.
			if ( edd_item_in_cart( $download_id, $cart_check_options ) ) {
				$this->restored_items[ $key ] = true;
				$restored_any                 = true;
				continue;
			}

			// Add to cart with stored options which preserves all the original data.
			$result = edd_add_to_cart( $download_id, $stored_options );

			if ( is_wp_error( $result ) || false === $result ) {
				continue;
			}

			$this->restored_items[ $key ] = true;
			$restored_any                 = true;
		}

		// Display user-friendly notice for unavailable items.
		if ( ! empty( $unavailable_items ) ) {
			$item_names = array();
			foreach ( $unavailable_items as $item_data ) {
				$item_names[] = $item_data['title'];
			}

			if ( ! empty( $item_names ) ) {
				$message = sprintf(
					/* translators: %s: list of product names */
					_n(
						'Welcome back! We\'ve restored your cart. Please note, "%s" is no longer available and has been removed.',
						'Welcome back! We\'ve restored your cart. Please note, the following items are no longer available and have been removed: %s',
						count( $item_names ),
						'easy-digital-downloads'
					),
					implode( '", "', $item_names )
				);
				\EDD\Utils\Messages::add( 'warn', 'edd_acr_unavailable_items', $message );
			}
		}

		if ( ! $restored_any && ! $has_existing_items ) {
			// Roll back to an empty cart when no items could be restored and no current items existed.
			edd_empty_cart();
		}

		return $restored_any;
	}

	/**
	 * Checks for price changes after cart rebuild and discount application.
	 *
	 * Compares saved prices against current cart prices (including discounts).
	 * Displays notices only for items with significant price differences.
	 *
	 * @since 3.6.5
	 * @return void
	 */
	private function check_price_changes(): void {
		if ( empty( $this->saved_items ) || empty( $this->restored_items ) ) {
			return;
		}

		$price_changes = array();

		foreach ( $this->restored_items as $key => $value ) {
			// Skip if we don't have saved price data for this item.
			if ( ! isset( $this->saved_items[ $key ] ) ) {
				continue;
			}

			$saved_item  = $this->saved_items[ $key ];
			$download_id = $saved_item['download_id'];
			$price_id    = $saved_item['price_id'];

			// Build options array to get the cart index.
			$options = array();
			if ( ! is_null( $price_id ) && '' !== $price_id ) {
				$options['price_id'] = $price_id;
			}

			// Get the actual cart index using edd_get_item_position_in_cart().
			$cart_index = edd_get_item_position_in_cart( $download_id, $options );
			if ( false === $cart_index ) {
				continue;
			}

			$current_raw_price = edd_get_cart_item_final_price( $cart_index );
			if ( false === $current_raw_price ) {
				continue;
			}

			$stored_price  = $saved_item['price'];
			$current_price = (float) $current_raw_price;

			// Only report if there's a significant difference.
			if ( $this->has_significant_price_difference( $stored_price, $current_price ) ) {
				$price_changes[] = array(
					'title'     => $saved_item['title'],
					'old_price' => $stored_price,
					'new_price' => $current_price,
				);
			}
		}

		if ( empty( $price_changes ) ) {
			return;
		}

		$formatted_changes = array();
		foreach ( $price_changes as $change ) {
			$formatted_changes[] = sprintf(
				/* translators: 1: Product title, 2: old price, 3: new price. */
				__( '%1$s (%2$s -> %3$s)', 'easy-digital-downloads' ),
				$change['title'],
				$this->format_price_for_notice( $change['old_price'] ),
				$this->format_price_for_notice( $change['new_price'] )
			);
		}

		$message = __( 'Note: Some prices have changed since your cart was saved.', 'easy-digital-downloads' );
		if ( ! empty( $formatted_changes ) ) {
			$message .= ' ' . sprintf(
				/* translators: %s: list of price change summaries. */
				__( 'Updated pricing: %s.', 'easy-digital-downloads' ),
				implode( '; ', $formatted_changes )
			);
		}

		\EDD\Utils\Messages::add( 'warn', 'edd_acr_price_changed', $message );
	}

	/**
	 * Builds a deterministic cart line key for merge checks.
	 *
	 * @since 3.6.5
	 * @param int         $download_id Download identifier.
	 * @param string|null $price_id    Price option identifier.
	 *
	 * @return string
	 */
	private function build_cart_line_key( int $download_id, ?string $price_id ): string {
		if ( is_null( $price_id ) ) {
			return $download_id;
		}

		return $download_id . '|' . $price_id;
	}

	/**
	 * Determines if the stored price differs from the current price enough to notify the shopper.
	 *
	 * @since 3.6.5
	 * @param float $stored_price  Stored price snapshot.
	 * @param float $current_price Current price at restore time.
	 *
	 * @return bool
	 */
	private function has_significant_price_difference( float $stored_price, float $current_price ): bool {
		return abs( $stored_price - $current_price ) > 0.01;
	}

	/**
	 * Formats prices for display within cart restoration notices.
	 *
	 * @since 3.6.5
	 * @param float $amount Monetary amount to format.
	 *
	 * @return string
	 */
	private function format_price_for_notice( float $amount ): string {
		$formatted = edd_format_amount( $amount );

		return edd_currency_filter( $formatted );
	}

	/**
	 * Gets the discount code to apply.
	 *
	 * This is called after the cart has been rebuilt, so we can validate
	 * the discount against the actual cart contents.
	 *
	 * @since 3.6.5
	 * @param \EDD\Pro\CartRecovery\Carts\Cart $cart The cart object.
	 * @return string Discount code.
	 */
	private function get_discount_code( $cart ) {
		$discount_code = $cart->discount_applied;

		// Check if discount code is in the URL.
		if ( ! empty( $_GET['acr-discount'] ) ) {
			$discount_code = sanitize_text_field( wp_unslash( $_GET['acr-discount'] ) );
		}

		// Fallback to saved cart discount.
		if ( empty( $discount_code ) ) {
			return '';
		}

		// Validate the discount before applying it.
		// At this point, the cart has been rebuilt, so we can validate against cart contents.
		if ( ! empty( $discount_code ) ) {
			$discount = edd_get_discount_by_code( $discount_code );
			if ( ! $discount ) {
				edd_debug_log( sprintf( '[EDD ACR] Discount code "%s" does not exist and will not be applied.', $discount_code ) );
				return '';
			}

			// Use is_valid() to check against current cart contents, product requirements, etc.
			if ( ! $discount->is_valid( '', false ) ) {
				edd_debug_log( sprintf( '[EDD ACR] Discount code "%s" is invalid for the current cart and will not be applied.', $discount_code ) );
				return '';
			}
		}

		return $discount_code;
	}
}
