<?php
/**
 * Genuine WooCommerce recent-sale collection.
 *
 * @package VoidPulse
 */

namespace VoidPulse\Notifications;

use VoidPulse\Infrastructure\EventRepository;
use VoidPulse\Privacy\SafeOrderFormatter;

defined( 'ABSPATH' ) || exit;

final class RecentSaleCollector {
	/**
	 * Order statuses whose activity may be displayed.
	 */
	const DISPLAYABLE_STATUSES = array( 'processing', 'completed' );

	/**
	 * Remove cached activity as soon as its order stops qualifying.
	 *
	 * The scheduled refresh rebuilds the cache from current orders, but a
	 * deleted, trashed, refunded or cancelled order must stop being displayed
	 * immediately rather than at the next refresh.
	 *
	 * @return void
	 */
	public function register() {
		/*
		 * These run at the earliest priority and use the order object supplied
		 * by the hook: by the default priority WooCommerce has already detached
		 * the line items, and the cached activity can no longer be identified.
		 */
		add_action( 'woocommerce_before_delete_order', array( $this, 'purge_order' ), 1, 2 );
		add_action( 'woocommerce_before_trash_order', array( $this, 'purge_order' ), 1, 2 );
		add_action( 'before_delete_post', array( $this, 'purge_order_post' ), 1 );
		add_action( 'wp_trash_post', array( $this, 'purge_order_post' ), 1 );
		add_action( 'woocommerce_order_status_changed', array( $this, 'purge_ineligible_order' ), 10, 3 );
	}

	/**
	 * Return the stable cache key for one order line item.
	 *
	 * @param int $order_id Order ID.
	 * @param int $item_id Line item ID.
	 * @return string
	 */
	public static function event_key( $order_id, $item_id ) {
		return hash_hmac( 'sha256', absint( $order_id ) . ':' . absint( $item_id ), wp_salt( 'nonce' ) );
	}

	/**
	 * Purge cached activity for a legacy post-table order.
	 *
	 * @param int $post_id Post ID.
	 * @return void
	 */
	public function purge_order_post( $post_id ) {
		if ( 'shop_order' === get_post_type( $post_id ) ) {
			$this->purge_order( $post_id );
		}
	}

	/**
	 * Purge cached activity when an order leaves a displayable status.
	 *
	 * @param int    $order_id Order ID.
	 * @param string $from Previous status.
	 * @param string $to New status.
	 * @return void
	 */
	public function purge_ineligible_order( $order_id, $from, $to ) {
		unset( $from );
		if ( ! in_array( $to, self::DISPLAYABLE_STATUSES, true ) ) {
			$this->purge_order( $order_id );
		}
	}

	/**
	 * Remove every cached event belonging to one order.
	 *
	 * @param int            $order_id Order ID.
	 * @param \WC_Order|null $order Order supplied by the hook, when available.
	 * @return int Number of cached events removed.
	 */
	public function purge_order( $order_id, $order = null ) {
		if ( ! $order instanceof \WC_Order ) {
			$order = function_exists( 'wc_get_order' ) ? wc_get_order( absint( $order_id ) ) : null;
		}
		if ( ! $order instanceof \WC_Order ) {
			return 0;
		}
		$keys = array();
		foreach ( array_keys( $order->get_items( 'line_item' ) ) as $item_id ) {
			$keys[] = self::event_key( $order->get_id(), $item_id );
		}
		return $keys ? ( new EventRepository() )->delete_by_keys( $keys ) : 0;
	}

	/**
	 * Collect a bounded batch.
	 *
	 * @param array<string,mixed> $campaign Validated campaign.
	 * @return int
	 */
	public function collect( $campaign ) {
		if ( ! function_exists( 'wc_get_orders' ) ) {
			return 0;
		}
		$orders_settings = $campaign['orders'];
		$orders_settings['statuses'] = array_values( array_intersect( (array) apply_filters( 'voidpulse_eligible_order_statuses', $orders_settings['statuses'], $campaign ), array( 'wc-processing', 'wc-completed' ) ) );
		if ( ! $orders_settings['statuses'] ) {
			return 0;
		}
		$orders          = wc_get_orders(
			array(
				'status'       => array_map( static function ( $status ) { return str_replace( 'wc-', '', $status ); }, $orders_settings['statuses'] ),
				'limit'        => min( 200, absint( $orders_settings['maximum'] ) ),
				'orderby'      => 'date',
				'order'        => 'DESC',
				'date_created' => '>' . ( time() - DAY_IN_SECONDS * absint( $orders_settings['days'] ) ),
				'return'       => 'objects',
			)
		);

		$eligibility = new OrderEligibility();
		$formatter   = new SafeOrderFormatter();
		$repository  = new EventRepository();
		$count       = 0;
		$product_ids = isset( $orders_settings['product_ids'] ) ? array_map( 'absint', (array) $orders_settings['product_ids'] ) : array();
		$product_rule = isset( $orders_settings['product_rule'] ) ? sanitize_key( $orders_settings['product_rule'] ) : 'all';
		$repository->clear_sales();

		foreach ( $orders as $order ) {
			if ( ! $eligibility->is_eligible( $order, $orders_settings ) ) {
				continue;
			}
			foreach ( $order->get_items( 'line_item' ) as $item_id => $item ) {
				$product = $item->get_product();
				if ( ! $product || ! $product->exists() || 'publish' !== get_post_status( $product->get_id() ) ) {
					continue;
				}
				$product_id   = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id();
				$variation_id = $product->is_type( 'variation' ) ? $product->get_id() : 0;
				$is_selected  = in_array( absint( $product_id ), $product_ids, true );
				if ( ( 'include' === $product_rule && ! $is_selected ) || ( 'exclude' === $product_rule && $is_selected ) ) {
					continue;
				}
				if ( ! $orders_settings['include_hidden_products'] && 'visible' !== $product->get_catalog_visibility() ) {
					continue;
				}
				$remaining_quantity = (float) $item->get_quantity() + (float) $order->get_qty_refunded_for_item( $item_id );
				if ( $remaining_quantity <= 0 ) {
					continue;
				}

				$date = $order->get_date_paid() ?: $order->get_date_created();
				if ( ! $date ) {
					continue;
				}
				$image_id     = $product->get_image_id();
				if ( ! $image_id && $variation_id ) {
					$parent   = wc_get_product( $product_id );
					$image_id = $parent ? $parent->get_image_id() : 0;
				}
				$payload = array(
					'customer'       => $formatter->customer( $order, $campaign['identity'] ),
					'location'       => $formatter->location( $order, $campaign['location'] ),
					'product'        => sanitize_text_field( $item->get_name() ),
					'product_url'    => esc_url_raw( get_permalink( $product_id ) ),
					'product_image'  => $image_id ? esc_url_raw( wp_get_attachment_image_url( $image_id, 'woocommerce_thumbnail' ) ) : '',
					'event_timestamp'=> $date->getTimestamp(),
				);
				$payload = apply_filters( 'voidpulse_event_view_model', $payload, $order, $item, $campaign );
				$payload = is_array( $payload ) ? array_intersect_key( $payload, array_flip( array( 'customer', 'location', 'product', 'product_url', 'product_image', 'event_timestamp' ) ) ) : array();
				$payload = array(
					'customer'        => sanitize_text_field( isset( $payload['customer'] ) ? $payload['customer'] : '' ),
					'location'        => sanitize_text_field( isset( $payload['location'] ) ? $payload['location'] : '' ),
					'product'         => sanitize_text_field( isset( $payload['product'] ) ? $payload['product'] : '' ),
					'product_url'     => esc_url_raw( isset( $payload['product_url'] ) ? $payload['product_url'] : '' ),
					'product_image'   => esc_url_raw( isset( $payload['product_image'] ) ? $payload['product_image'] : '' ),
					'event_timestamp' => absint( isset( $payload['event_timestamp'] ) ? $payload['event_timestamp'] : $date->getTimestamp() ),
				);
				$event   = array(
					'source_record_id' => self::event_key( $order->get_id(), $item_id ),
					'product_id'        => $product_id,
					'variation_id'      => $variation_id,
					'event_timestamp'   => gmdate( 'Y-m-d H:i:s', $date->getTimestamp() ),
					'expires_at'        => gmdate( 'Y-m-d H:i:s', time() + DAY_IN_SECONDS * max( 1, absint( $orders_settings['days'] ) ) ),
					'payload'           => $payload,
				);
				if ( $repository->upsert( $event ) ) {
					++$count;
				}
			}
		}

		return $count;
	}
}
