<?php
/**
 * Telling staff a product is running out.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango\WooCommerce;

use TextTango\Api\PhoneNumber;
use TextTango\Messages\MessageLog;
use TextTango\Sending\Sender;
use TextTango\Support\Clock;
use WC_Product;

defined( 'ABSPATH' ) || exit;

/**
 * Low-stock alerts to the store's own numbers (spec §6).
 *
 * ### Why this one needs a cooldown and the order alerts do not
 *
 * `woocommerce_low_stock` fires on **every** stock change while the level is
 * below the threshold, not once when it crosses. A product sitting at two with
 * a threshold of three fires again on every sale, every manual edit, every
 * stock sync — and on a busy store that is a text a minute about the same
 * product.
 *
 * A dedupe key alone cannot fix that, because each firing is genuinely a
 * different moment and the level has genuinely changed. So the rule is time
 * based: {@see self::COOLDOWN} per product, remembered against the product
 * itself. The merchant is told once a day that something is low, which is how
 * often that fact is worth acting on.
 */
final class Stock {

	/**
	 * Event id.
	 */
	public const EVENT = 'low_stock_admin';

	/**
	 * The shortest time between alerts about the same product.
	 *
	 * Not a setting: this is a runaway guard, not a preference. WooCommerce
	 * re-fires the hook on every stock change below the threshold, so without it
	 * a busy product texts the whole team on every sale.
	 */
	public const COOLDOWN = DAY_IN_SECONDS;

	/**
	 * Product meta holding when this product was last reported.
	 */
	public const META_ALERTED = '_texttango_stock_alerted';

	/**
	 * Register hooks.
	 */
	public function register(): void {
		add_action( 'woocommerce_low_stock', array( self::class, 'on_low_stock' ) );

		// Selling out is the one worth knowing about most, and WooCommerce fires
		// it separately rather than as a low-stock event with a level of zero.
		add_action( 'woocommerce_no_stock', array( self::class, 'on_low_stock' ) );
	}

	/**
	 * A product is running out.
	 *
	 * @param WC_Product|mixed $product Product, as WooCommerce passes it.
	 */
	public static function on_low_stock( $product ): void {
		if ( ! $product instanceof WC_Product || ! Events::is_enabled( self::EVENT ) ) {
			return;
		}

		$now = Clock::now();

		if ( ! self::due( (int) $product->get_meta( self::META_ALERTED, true ), $now ) ) {
			return;
		}

		$body = Templates::render(
			Events::template( self::EVENT ),
			Templates::stock_values( $product )
		);

		/** This filter is documented in src/WooCommerce/Notifications.php */
		$body = (string) apply_filters( 'texttango_message_template', $body, self::EVENT, null );

		if ( '' === trim( $body ) ) {
			return;
		}

		/*
		 * Stamped before sending, not after. If the send fails the merchant loses
		 * one alert; if the stamp fails they get one per sale, and the second
		 * failure is the one that makes people switch the feature off.
		 */
		$product->update_meta_data( self::META_ALERTED, (string) $now );
		$product->save();

		self::alert_staff( $body, (int) $product->get_id(), $now );
	}

	/**
	 * Whether enough time has passed since the last alert about a product.
	 *
	 * Pure, so the rule can be read and tested without a product behind it.
	 *
	 * @param int $last When this product was last reported. 0 means never.
	 * @param int $now  Current timestamp.
	 */
	public static function due( int $last, int $now ): bool {
		if ( $last <= 0 ) {
			return true;
		}

		return $now - $last >= self::COOLDOWN;
	}

	/**
	 * Send one copy to each staff number.
	 *
	 * @param string $body       Rendered message.
	 * @param int    $product_id Product the alert is about.
	 * @param int    $now        Current timestamp, for the dedupe key.
	 */
	private static function alert_staff( string $body, int $product_id, int $now ): void {
		// Already normalized and made distinct by admin_recipients(), so one
		// handset written two ways cannot become two messages here.
		foreach ( Events::admin_recipients() as $index => $recipient ) {
			Sender::queue(
				array(
					'to'         => $recipient,
					'message'    => $body,
					'event'      => self::EVENT,
					// The day, not the moment, so two hooks firing in the same
					// second for one product cannot produce two alerts. The index
					// keeps each staff number's copy distinct.
					'dedupe_key' => MessageLog::build_dedupe_key(
						self::EVENT,
						(string) $product_id,
						gmdate( 'Y-m-d', $now ) . ':' . $index
					),
				)
			);
		}
	}
}
