<?php
/**
 * Keeping an eye on the account balance.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango\Sending;

use TextTango\Admin\Ajax;
use TextTango\Api\Client;
use TextTango\Support\Clock;
use TextTango\Support\Settings;

defined( 'ABSPATH' ) || exit;

/**
 * Watches the wallet so running out is noticed before it bites (spec §19).
 *
 * Running dry is not a graceful failure. Every send starts returning 402, order
 * updates stop, and — worst — **sign-in codes stop**, so customers cannot get
 * into their accounts. The first anyone usually hears of it is a support ticket.
 *
 * Read on a schedule rather than before each send. Checking the wallet per
 * message would double the API traffic and put a second network round trip in
 * front of every login; a figure that is an hour old is plenty to decide whether
 * a merchant should be told to top up.
 *
 * The cached figure is also refreshed opportunistically whenever anything else
 * reads the wallet — the connection test, Site Health, the CLI — so an admin who
 * has just looked at their balance is not told a stale one.
 */
final class BalanceWatch {

	/**
	 * Action Scheduler hook for the check.
	 */
	public const HOOK = 'texttango_check_balance';

	/**
	 * How often it runs.
	 */
	public const INTERVAL = HOUR_IN_SECONDS;

	/**
	 * Option holding the last reading. Never autoloaded.
	 */
	public const OPTION = 'texttango_balance';

	/**
	 * Register hooks.
	 */
	public function register(): void {
		add_action( self::HOOK, array( self::class, 'run' ) );
		add_action( 'init', array( self::class, 'ensure_scheduled' ) );
	}

	/**
	 * Make sure the hourly check exists.
	 */
	public static function ensure_scheduled(): void {
		if ( ! function_exists( 'as_has_scheduled_action' ) || ! function_exists( 'as_schedule_recurring_action' ) ) {
			return;
		}

		if ( as_has_scheduled_action( self::HOOK, array(), Queue::GROUP ) ) {
			return;
		}

		as_schedule_recurring_action( time() + self::INTERVAL, self::INTERVAL, self::HOOK, array(), Queue::GROUP );
	}

	/**
	 * Read the wallet and remember what it said.
	 *
	 * @return float The balance, or -1 when it could not be read.
	 */
	public static function run(): float {
		$client = Client::from_settings();

		if ( is_wp_error( $client ) ) {
			return -1.0;
		}

		$response = $client->wallet();

		if ( is_wp_error( $response ) ) {
			return -1.0;
		}

		$wallet = Ajax::parse_wallet( (array) $response );

		self::remember( (float) $wallet['main'], (string) $wallet['currency'] );

		return (float) $wallet['main'];
	}

	/**
	 * Store a reading.
	 *
	 * @param float  $amount   Balance.
	 * @param string $currency Currency code.
	 */
	public static function remember( float $amount, string $currency ): void {
		update_option(
			self::OPTION,
			array(
				'amount'   => $amount,
				'currency' => $currency,
				'at'       => Clock::now(),
			),
			false
		);
	}

	/**
	 * The whole reading, with defaults for a site that has never checked.
	 *
	 * @return array{amount: float, currency: string, at: int}
	 */
	public static function last(): array {
		$stored = get_option( self::OPTION, array() );
		$stored = is_array( $stored ) ? $stored : array();

		return array(
			'amount'   => isset( $stored['amount'] ) ? (float) $stored['amount'] : -1.0,
			'currency' => isset( $stored['currency'] ) ? (string) $stored['currency'] : '',
			'at'       => isset( $stored['at'] ) ? (int) $stored['at'] : 0,
		);
	}

	/**
	 * The last known balance, or -1 if it has never been read.
	 *
	 * Deliberately -1 rather than 0. A balance of zero means "no credit, pause
	 * things"; never having looked means nothing at all, and treating the two
	 * alike would pause a working site the moment this shipped.
	 */
	public static function last_known(): float {
		return self::last()['amount'];
	}

	/**
	 * Whether the balance is low enough to warn about.
	 */
	public static function is_low(): bool {
		$warn    = (float) Settings::get( 'balance_warn_below', 0 );
		$balance = self::last_known();

		return $warn > 0.0 && $balance >= 0.0 && $balance < $warn;
	}

	/**
	 * The reading formatted for display, or '' if there has never been one.
	 */
	public static function formatted(): string {
		$last = self::last();

		if ( $last['amount'] < 0.0 ) {
			return '';
		}

		return trim( $last['currency'] . ' ' . number_format_i18n( $last['amount'], 2 ) );
	}
}
