<?php
/**
 * Монитор смены цен: хуки на post meta (core).
 *
 * Ловит любые изменения _regular_price/_sale_price товаров и вариаций,
 * из любого источника, пишущего через WordPress API (ручная правка,
 * quick edit, REST, импортёры, сторонние плагины). Каждое реальное
 * изменение публикуется хуком e2w_price_change_alerts_price_changed.
 *
 * @package E2W_Price_Change_Alerts
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class E2W_Price_Change_Alerts_Price_Monitor
 */
class E2W_Price_Change_Alerts_Price_Monitor {

	/**
	 * Отслеживаемые мета-ключи. Служебный _price не мониторим — производное.
	 *
	 * @var string[]
	 */
	private $meta_keys = array( '_regular_price', '_sale_price' );

	/**
	 * Старые значения, снятые до записи. Ключ: "{object_id}:{meta_key}".
	 *
	 * @var array
	 */
	private $old_values = array();

	/**
	 * Меты, удалённые в этом запросе. Ключ: "{object_id}:{meta_key}".
	 * Нужны, чтобы отличить re-add после delete (массовые синки пишут цену
	 * паттерном delete→add) от добавления цены новому товару.
	 *
	 * @var array
	 */
	private $deleted_keys = array();

	/**
	 * Регистрация хуков.
	 */
	public function __construct() {
		add_action( 'update_post_meta', array( $this, 'remember_before_update' ), 10, 4 );
		add_action( 'updated_post_meta', array( $this, 'detect_update' ), 10, 4 );
		add_action( 'delete_post_meta', array( $this, 'remember_before_delete' ), 10, 4 );
		add_action( 'deleted_post_meta', array( $this, 'detect_delete' ), 10, 4 );
		add_action( 'added_post_meta', array( $this, 'detect_add' ), 10, 4 );
	}

	/**
	 * Интересует ли нас эта мета этого объекта.
	 *
	 * @param int    $object_id Post ID.
	 * @param string $meta_key  Meta key.
	 * @return bool
	 */
	private function is_watched( $object_id, $meta_key ) {
		if ( ! in_array( $meta_key, $this->meta_keys, true ) ) {
			return false;
		}

		return in_array( get_post_type( $object_id ), array( 'product', 'product_variation' ), true );
	}

	/**
	 * Снять старое значение до обновления меты.
	 *
	 * @param int    $meta_id    Meta ID.
	 * @param int    $object_id  Post ID.
	 * @param string $meta_key   Meta key.
	 * @param mixed  $meta_value Новое значение (ещё не записано).
	 */
	public function remember_before_update( $meta_id, $object_id, $meta_key, $meta_value ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
		if ( ! $this->is_watched( $object_id, $meta_key ) ) {
			return;
		}

		$this->old_values[ $object_id . ':' . $meta_key ] = (string) get_post_meta( $object_id, $meta_key, true );
	}

	/**
	 * Мета обновлена — сравнить и опубликовать изменение.
	 *
	 * @param int    $meta_id    Meta ID.
	 * @param int    $object_id  Post ID.
	 * @param string $meta_key   Meta key.
	 * @param mixed  $meta_value Записанное значение.
	 */
	public function detect_update( $meta_id, $object_id, $meta_key, $meta_value ) {
		$key = $object_id . ':' . $meta_key;

		if ( ! array_key_exists( $key, $this->old_values ) ) {
			return;
		}

		$old = $this->old_values[ $key ];
		unset( $this->old_values[ $key ] );

		$this->dispatch( $object_id, $meta_key, $old, (string) $meta_value );
	}

	/**
	 * Снять старое значение до удаления меты.
	 *
	 * @param array  $meta_ids   Meta IDs.
	 * @param int    $object_id  Post ID.
	 * @param string $meta_key   Meta key.
	 * @param mixed  $meta_value Значение-фильтр удаления.
	 */
	public function remember_before_delete( $meta_ids, $object_id, $meta_key, $meta_value ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
		if ( ! $this->is_watched( $object_id, $meta_key ) ) {
			return;
		}

		$this->old_values[ $object_id . ':' . $meta_key ] = (string) get_post_meta( $object_id, $meta_key, true );
	}

	/**
	 * Мета удалена (цена снята) — опубликовать изменение old → ''.
	 *
	 * @param array  $meta_ids   Meta IDs.
	 * @param int    $object_id  Post ID.
	 * @param string $meta_key   Meta key.
	 * @param mixed  $meta_value Значение-фильтр удаления.
	 */
	public function detect_delete( $meta_ids, $object_id, $meta_key, $meta_value ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
		$key = $object_id . ':' . $meta_key;

		if ( ! array_key_exists( $key, $this->old_values ) ) {
			return;
		}

		$old = $this->old_values[ $key ];
		unset( $this->old_values[ $key ] );

		$this->deleted_keys[ $key ] = true;

		$this->dispatch( $object_id, $meta_key, $old, '' );
	}

	/**
	 * Мета добавлена. Интересует только re-add после delete в этом же
	 * запросе: коллектор схлопнет пару delete+add в одно изменение
	 * (или в ничто, если цена в итоге не поменялась). Добавление цены
	 * новому товару изменением не считается.
	 *
	 * @param int    $meta_id    Meta ID.
	 * @param int    $object_id  Post ID.
	 * @param string $meta_key   Meta key.
	 * @param mixed  $meta_value Добавленное значение.
	 */
	public function detect_add( $meta_id, $object_id, $meta_key, $meta_value ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
		$key = $object_id . ':' . $meta_key;

		if ( ! isset( $this->deleted_keys[ $key ] ) ) {
			return;
		}

		unset( $this->deleted_keys[ $key ] );

		$this->dispatch( $object_id, $meta_key, '', (string) $meta_value );
	}

	/**
	 * Опубликовать изменение, если оно реальное.
	 *
	 * @param int    $object_id Post ID.
	 * @param string $meta_key  Meta key.
	 * @param string $old       Старое значение.
	 * @param string $new       Новое значение.
	 */
	private function dispatch( $object_id, $meta_key, $old, $new ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.newFound
		if ( $old === $new ) {
			return;
		}

		// '10' и '10.00' — одна и та же цена.
		if ( is_numeric( $old ) && is_numeric( $new ) && (float) $old === (float) $new ) {
			return;
		}

		// Точка расширения: premium-режим «Only selected products» может
		// отключить товар. В free фильтр всегда true.
		if ( ! apply_filters( 'e2w_price_change_alerts_is_tracked', true, (int) $object_id ) ) {
			return;
		}

		$change = array(
			'product_id' => (int) $object_id,
			'price_type' => '_regular_price' === $meta_key ? 'regular' : 'sale',
			'old'        => $old,
			'new'        => $new,
		);

		do_action( 'e2w_price_change_alerts_price_changed', $change );
	}
}
