<?php
/**
 * Snapshot Builder for Cart Recovery
 *
 * Builds cart snapshots from server-side data with optimized database queries
 * and proper data sanitization.
 *
 * @package EDD\Pro\CartRecovery\Snapshots
 * @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\Snapshots;

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

use EDD\Pro\CartRecovery\Carts\Repository;
use EDD\Pro\CartRecovery\Config\Security;

/**
 * Snapshot Builder class.
 *
 * Responsible for building cart snapshots from the current server-side state,
 * including cart contents, customer information, and session data.
 */
class SnapshotBuilder {

	/**
	 * Builds a complete snapshot from server-side cart data.
	 *
	 * @since 3.6.5
	 * @return array Complete snapshot payload.
	 */
	public function build_server_snapshot(): array {
		$contents = edd_get_cart_content_details();
		$items    = array();

		// Always get identifiers even if cart is empty - needed to update existing carts on purchase completion.
		$customer_id = $this->get_customer_id();
		$email       = $this->get_checkout_email();
		$session_id  = $this->get_session_id();
		$cart_id     = EDD()->session->get( 'acr_cart' );

		if ( empty( $contents ) ) {
			// Cart is empty but we still need to return identifiers so we can find and update existing carts.
			// This is critical for marking carts as recovered after purchase completion.
			if ( empty( $session_id ) && empty( $customer_id ) && empty( $email ) && empty( $cart_id ) ) {
				return array(); // No identifiers available.
			}

			// Return minimal payload with just identifiers.
			$payload = array(
				'session_id'  => $session_id,
				'customer_id' => $customer_id,
				'email'       => $email,
			);

			// Include cart ID if available to ensure we update the correct cart.
			if ( ! empty( $cart_id ) ) {
				$payload['id'] = (int) $cart_id;
			}

			return $payload;
		}

		// Store the entire cart contents to preserve all data.
		// This includes recurring details, upgrades, license renewals, etc.
		if ( is_array( $contents ) ) {
			$items = array_slice( $contents, 0, Security::get_max_items_per_cart() );
		}

		$payload = array(
			'cart_contents'       => array( 'items' => $items ),
			'cart_total'          => (float) edd_get_cart_total(),
			'currency'            => sanitize_text_field( (string) edd_get_currency() ),
			'discount_applied'    => $this->get_first_discount(),
			'session_id'          => $session_id,
			'customer_id'         => $customer_id,
			'email'               => $email,
			'ip_address'          => substr( \sanitize_text_field( (string) edd_get_ip() ), 0, 45 ),
			'user_agent'          => ! empty( $_SERVER['HTTP_USER_AGENT'] ) ? substr( \sanitize_textarea_field( \wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), 0, 65535 ) : null,
			'cart_version'        => null,
			'timezone'            => $this->get_user_timezone(),
			'browser_fingerprint' => $this->get_browser_fingerprint(),
			'status'              => 'tracking',
		);

		// Include cart ID if available to ensure we update the correct cart.
		// This is critical when session ID changes (e.g., user account creation during checkout).
		if ( ! empty( $cart_id ) ) {
			$payload['id'] = (int) $cart_id;
		}

		$payload['date_modified'] = edd_get_utc_date_string();

		if ( ! empty( $payload['session_id'] ) || ! empty( $payload['customer_id'] ) || ! empty( $payload['email'] ) || ! empty( $cart_id ) ) {
			$payload = $this->hydrate_from_existing_cart( $payload );
		}

		return $payload;
	}

	/**
	 * Hydrates a snapshot with data from an existing cart.
	 *
	 * Prevents loss of client-side data during server-side updates.
	 * Preserves email and customer_id from existing carts so that cart updates
	 * (like discount codes) don't lose the recovery identity.
	 *
	 * @since 3.6.5
	 * @param array $payload Partially built snapshot payload.
	 * @return array Hydrated payload.
	 */
	private function hydrate_from_existing_cart( array $payload ): array {
		$cart_id  = EDD()->session->get( 'acr_cart' );
		$existing = null;
		if ( ! empty( $cart_id ) ) {
			$query = new \EDD\Pro\CartRecovery\Database\Queries\RecoveryCart();
			$cart  = $query->get_item( (int) $cart_id );
			if ( $cart && ! empty( $cart->id ) ) {
				$existing = $cart;
			}
		} else {
			$repository = new Repository();
			$existing   = $repository->find_primary_cart(
				array(
					'session_id'  => $payload['session_id'] ?? null,
					'customer_id' => $payload['customer_id'] ?? null,
					'email'       => $payload['email'] ?? null,
				)
			);
		}

		if ( ! $existing ) {
			return $payload;
		}

		// Preserve email from existing cart if current payload has none.
		// This ensures cart updates (discounts, quantity changes) don't lose the email.
		if ( empty( $payload['email'] ) && ! empty( $existing->email ) ) {
			$payload['email'] = sanitize_email( (string) $existing->email );
		}

		// Preserve customer_id from existing cart if current payload has none.
		if ( empty( $payload['customer_id'] ) && ! empty( $existing->customer_id ) ) {
			$payload['customer_id'] = (int) $existing->customer_id;
		}

		// If the current snapshot has no timezone, but the existing cart does, carry it over.
		if ( is_null( $payload['timezone'] ) && ! empty( $existing->timezone ) && $this->is_valid_timezone( $existing->timezone ) ) {
			$payload['timezone'] = sanitize_text_field( (string) $existing->timezone );
		}

		return $payload;
	}

	/**
	 * Resolves checkout timezone.
	 *
	 * Attempts to retrieve timezone from cookie (set client-side on page load).
	 *
	 * @since 3.6.5
	 * @return string|null Timezone string or null.
	 */
	private function get_user_timezone(): ?string {
		// Check if timezone was captured via cookie from client-side detection.
		if ( ! empty( $_COOKIE['edd_acr_timezone'] ) ) {
			$timezone = sanitize_text_field( wp_unslash( $_COOKIE['edd_acr_timezone'] ) );
			if ( $this->is_valid_timezone( $timezone ) ) {
				return $timezone;
			}
		}

		return null;
	}

	/**
	 * Gets browser fingerprint from cookie.
	 *
	 * @since 3.6.5
	 * @return string|null Browser fingerprint or null.
	 */
	private function get_browser_fingerprint(): ?string {
		if ( ! empty( $_COOKIE['edd_acr_fingerprint'] ) ) {
			return substr( sanitize_text_field( wp_unslash( $_COOKIE['edd_acr_fingerprint'] ) ), 0, 255 );
		}

		return null;
	}

	/**
	 * Retrieves the first applied discount code.
	 *
	 * @since 3.6.5
	 * @return string|null First discount code or null.
	 */
	private function get_first_discount(): ?string {
		$discounts = (array) edd_get_cart_discounts();

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

		$first = reset( $discounts );

		return $first ? sanitize_text_field( (string) $first ) : null;
	}

	/**
	 * Retrieves checkout email if available.
	 *
	 * Checks multiple sources in priority order:
	 * 1. Checkout form fields (edd_email, edd_email_confirm)
	 * 2. Email validation AJAX (email field from edd_check_email)
	 * 3. Logged-in user's email
	 *
	 * @since 3.6.5
	 * @return string|null Email address or null.
	 */
	private function get_checkout_email(): ?string {

		// Prioritize checkout form emails as they represent current user intent.
		if ( isset( $_POST['edd_email'] ) ) {
			return sanitize_email( wp_unslash( $_POST['edd_email'] ) );
		}

		if ( isset( $_POST['edd_email_confirm'] ) ) {
			return sanitize_email( wp_unslash( $_POST['edd_email_confirm'] ) );
		}

		if ( isset( $_POST['email'] ) ) {
			// EDD's edd_check_email AJAX endpoint uses 'email' field.
			return sanitize_email( wp_unslash( $_POST['email'] ) );
		}

		$user = wp_get_current_user();
		if ( $user->exists() ) {
			return sanitize_email( $user->user_email );
		}

		return null;
	}

	/**
	 * Retrieves customer ID if logged-in.
	 *
	 * @since 3.6.5
	 * @return int|null Customer ID or null.
	 */
	private function get_customer_id(): ?int {
		if ( ! is_user_logged_in() ) {
			$email = EDD()->session->get( 'email_validated' );
			if ( ! empty( $email ) && is_email( $email ) ) {
				$customer = edd_get_customer_by( 'email', $email );
				if ( $customer instanceof \EDD_Customer ) {
					return (int) $customer->id;
				}
			}

			return null;
		}

		$user = wp_get_current_user();
		if ( $user->exists() ) {
			$customer = edd_get_customer_by( 'user_id', $user->ID );
			if ( $customer instanceof \EDD_Customer ) {
				return (int) $customer->id;
			}
		}

		return null;
	}

	/**
	 * Validates timezone string against installed timezone identifiers.
	 *
	 * @since 3.6.5
	 * @param string $timezone Timezone value.
	 * @return bool True if valid timezone.
	 */
	private function is_valid_timezone( string $timezone ): bool {
		if ( '' === $timezone ) {
			return false;
		}

		static $valid;
		if ( null === $valid ) {
			$valid = timezone_identifiers_list();
		}

		return in_array( $timezone, $valid, true );
	}

	/**
	 * Retrieves the session ID from the EDD session cookie.
	 *
	 * @since 3.6.5
	 * @return string Session ID.
	 */
	private function get_session_id(): string {
		EDD()->session->maybe_start_session( true );
		$cookie = EDD()->session->get_session_cookie();
		if ( is_array( $cookie ) && ! empty( $cookie['session_key'] ) ) {
			return sanitize_text_field( (string) $cookie['session_key'] );
		}

		return '';
	}
}
