<?php
/**
 * Delete carts and related email records and journey events.
 *
 * @package EDD\Pro\CartRecovery\Carts
 * @copyright Copyright (c) 2026, Sandhills Development, LLC
 * @license https://opensource.org/licenses/GPL-2.0 GNU General Public License
 * @since 3.6.5
 */

namespace EDD\Pro\CartRecovery\Carts;

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

/**
 * Delete carts and related email records and journey events.
 */
class Delete {

	/**
	 * MySQL datetime format for queries.
	 *
	 * @since 3.6.5
	 * @var string
	 */
	private $date_format = 'Y-m-d H:i:s';

	/**
	 * Deletes carts (and related email rows and journey events) that became stale before the timestamp.
	 *
	 * IMPORTANT: This method preserves recovered carts
	 * to maintain historical conversion data for accurate reporting. Only carts in
	 * deletable statuses (default: 'expired', 'abandoned') are removed.
	 *
	 * @since 3.6.5
	 * @param int $timestamp Unix timestamp in seconds.
	 * @return int Number of carts removed.
	 * @throws \Exception If the query preparation fails.
	 */
	public function delete_carts_before( int $timestamp ): int {
		$threshold = $this->to_mysql_datetime( $timestamp );
		if ( empty( $threshold ) ) {
			$days      = edd_get_option( 'acr_retention_days', 30 );
			$threshold = gmdate( $this->date_format, time() - ( $days * DAY_IN_SECONDS ) );
		}

		$statuses = $this->get_cleanup_statuses();
		if ( empty( $statuses ) ) {
			return 0;
		}

		global $wpdb;
		$statuses            = array_map( 'sanitize_text_field', array_map( 'strval', $statuses ) );
		$status_placeholders = $this->get_placeholders( $statuses );

		$select_sql = "SELECT id FROM {$wpdb->prefix}edd_acr_carts
			WHERE status IN ( {$status_placeholders} )
			AND date_modified <= %s";

		$select_params   = array_merge( $statuses, array( $threshold ) );
		$prepared_select = $wpdb->prepare( $select_sql, ...$select_params );

		if ( false === $prepared_select ) {
			return 0;
		}

		$cart_ids = $wpdb->get_col( $prepared_select );
		if ( empty( $cart_ids ) ) {
			return 0;
		}

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

		try {
			$placeholders = $this->get_placeholders( $cart_ids, 'int' );

			// Delete journey events first.
			$delete_journey_sql      = "DELETE FROM {$wpdb->prefix}edd_acr_journey_events WHERE cart_id IN ( {$placeholders} )";
			$prepared_journey_delete = $wpdb->prepare( $delete_journey_sql, ...$cart_ids );
			if ( false === $prepared_journey_delete ) {
				throw new \Exception( 'Failed to prepare journey event deletion query.' );
			}

			$journey_deleted = $wpdb->query( $prepared_journey_delete );
			if ( false === $journey_deleted ) {
				throw new \Exception( 'Journey event deletion query failed.' );
			}

			// Delete recovery emails.
			$delete_emails_sql     = "DELETE FROM {$wpdb->prefix}edd_acr_emails WHERE cart_id IN ( {$placeholders} )";
			$prepared_email_delete = $wpdb->prepare( $delete_emails_sql, ...$cart_ids );
			if ( false === $prepared_email_delete ) {
				throw new \Exception( 'Failed to prepare email deletion query.' );
			}

			$email_deleted = $wpdb->query( $prepared_email_delete );
			if ( false === $email_deleted ) {
				throw new \Exception( 'Email deletion query failed.' );
			}

			// Delete carts.
			$delete_carts_sql     = "DELETE FROM {$wpdb->prefix}edd_acr_carts WHERE id IN ( {$placeholders} )";
			$prepared_cart_delete = $wpdb->prepare( $delete_carts_sql, ...$cart_ids );
			if ( false === $prepared_cart_delete ) {
				throw new \Exception( 'Failed to prepare cart deletion query.' );
			}

			$deleted = $wpdb->query( $prepared_cart_delete );

			if ( false === $deleted ) {
				throw new \Exception( 'Cart deletion query failed.' );
			}

			$wpdb->query( 'COMMIT' );

			return (int) $deleted;

		} catch ( \Exception $exception ) {
			$wpdb->query( 'ROLLBACK' );

			return 0;
		}
	}

	/**
	 * Deletes recovery emails created before a specific date.
	 *
	 * This method only deletes orphaned emails or emails associated with carts
	 * that are eligible for cleanup. Emails linked to 'recovered' or 'converted'
	 * carts are preserved to maintain accurate historical reporting data.
	 *
	 * @since 3.6.5
	 * @param int $timestamp Unix timestamp in seconds.
	 * @return int Number of deleted email records.
	 */
	public function delete_emails_before( int $timestamp ): int {
		if ( empty( $timestamp ) ) {
			return 0;
		}

		$threshold = $this->to_mysql_datetime( $timestamp );
		if ( empty( $threshold ) ) {
			$days      = edd_get_option( 'acr_retention_days', 30 );
			$threshold = gmdate( $this->date_format, time() - ( $days * DAY_IN_SECONDS ) );
		}

		if ( empty( $threshold ) ) {
			return 0;
		}

		global $wpdb;
		// Delete emails that are either:
		// 1. Orphaned (cart_id doesn't exist in carts table), OR
		// 2. Linked to carts with statuses that are eligible for cleanup (not recovered/converted).
		$carts_table = $wpdb->prefix . 'edd_acr_carts';
		$sql         = $wpdb->prepare(
			"DELETE e FROM {$wpdb->prefix}edd_acr_emails e
			LEFT JOIN {$carts_table} c ON e.cart_id = c.id
			WHERE e.date_sent < %s
			AND (
				c.id IS NULL
				OR c.status NOT IN ('recovered')
			)",
			$threshold
		);

		$deleted = $wpdb->query( $sql );

		$error = isset( $wpdb->last_error ) ? trim( (string) $wpdb->last_error ) : '';
		if ( '' !== $error ) {
			return 0;
		}

		return false === $deleted ? 0 : (int) $deleted;
	}

	/**
	 * Converts a value into a MySQL datetime string when possible.
	 *
	 * @since 3.6.5
	 * @param mixed $value Value to convert.
	 * @return string|null
	 */
	private function to_mysql_datetime( $value ) {
		if ( empty( $value ) ) {
			return null;
		}

		$timestamp = is_numeric( $value ) ? (int) $value : strtotime( (string) $value );
		if ( false === $timestamp ) {
			return null;
		}

		return gmdate( $this->date_format, $timestamp );
	}

	/**
	 * Returns the statuses that should be deleted during cleanup.
	 *
	 * @since 3.6.5
	 * @return array
	 */
	private function get_cleanup_statuses(): array {
		return array( 'expired', 'abandoned' );
	}

	/**
	 * Returns the SQL WHERE clause fragment for identifying empty carts.
	 *
	 * A cart is considered empty when it has no monetary value and no product items.
	 * Uses LIKE matching for cart_contents to handle JSON payloads that contain
	 * metadata (e.g. customer info) alongside an empty items array.
	 *
	 * @since 3.6.5
	 * @return string SQL WHERE clause fragment (without leading AND).
	 */
	public static function get_empty_cart_where_clause(): string {
		return "( cart_total = 0 OR cart_total IS NULL )
			AND ( cart_contents IS NULL OR cart_contents = '' OR cart_contents = '[]' OR cart_contents = '{}' OR cart_contents LIKE '%\"items\":[]%' )";
	}

	/**
	 * Returns placeholders for wpdb operations.
	 *
	 * @since 3.6.5
	 * @param array  $values Values being persisted.
	 * @param string $type   Type of placeholder to return.
	 * @return string
	 */
	private function get_placeholders( array $values, $type = 'string' ): string {
		if ( 'int' === $type ) {
			return implode( ',', array_fill( 0, count( $values ), '%d' ) );
		}

		return implode( ',', array_fill( 0, count( $values ), '%s' ) );
	}
}
