<?php
/**
 * BigDecimal formatting & arithmetic helpers shared by block renderers.
 *
 * Extracts the identical utility methods previously duplicated in
 * CRToday_Exchange_Rates_Block and CRToday_Ticker_Block.
 *
 * @package CRTodayCurrencyBlocks
 */

declare(strict_types=1);

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

trait CRToday_Decimal_Helpers {

	/**
	 * Validate and return a numeric value as a positive-only string,
	 * or null if the value is non-numeric / zero / negative.
	 */
	private static function normalize_to_string($value): ?string {
		if (! is_numeric($value)) {
			return null;
		}
		$str = (string) $value;
		if (CRToday_BigDecimal::of($str)->isLessThanOrEqualTo(CRToday_BigDecimal::zero())) {
			return null;
		}
		return $str;
	}

	/**
	 * Validate and return a numeric value as a finite float, or null.
	 */
	private static function normalize_to_float($value): ?float {
		if (! is_numeric($value)) {
			return null;
		}
		$float = (float) $value;
		return is_finite($float) ? $float : null;
	}

	/**
	 * Map the admin rounding-mode setting to a CRToday_RoundingMode constant.
	 */
	private static function resolve_rounding_mode(): int {
		$config = CRToday_Plugin_Settings::get_config();
		$mode   = $config['rounding_mode'] ?? 'half_even';
		switch ($mode) {
			case 'half_even':
				return CRToday_RoundingMode::HALF_EVEN;
			case 'down':
				return CRToday_RoundingMode::DOWN;
			default:
				return CRToday_RoundingMode::HALF_UP;
		}
	}

	/**
	 * Internal precision for intermediate calculations.
	 */
	private static function internal_scale(int $decimals): int {
		return max($decimals + 4, 12);
	}

	/**
	 * Return 1 / $value at the internal scale, or null for non-positive values.
	 */
	private static function bd_invert(string $value, int $decimals): ?string {
		$bd = CRToday_BigDecimal::of($value);
		if ($bd->isLessThanOrEqualTo(CRToday_BigDecimal::zero())) {
			return null;
		}
		return (string) CRToday_BigDecimal::one()
			->dividedBy($bd, self::internal_scale($decimals), CRToday_RoundingMode::HALF_UP);
	}

	/**
	 * $a × $b at the internal scale.
	 */
	private static function bd_multiply(string $a, string $b, int $decimals): string {
		return (string) CRToday_BigDecimal::of($a)
			->multipliedBy(CRToday_BigDecimal::of($b))
			->toScale(self::internal_scale($decimals), CRToday_RoundingMode::HALF_UP);
	}

	/**
	 * Compare two decimal strings: −1 / 0 / +1.
	 */
	private static function bd_compare(string $a, string $b): int {
		return CRToday_BigDecimal::of($a)->compareTo(CRToday_BigDecimal::of($b));
	}

	/**
	 * Sanitise a user-entered amount string (strip non-numeric chars, ensure > 0).
	 */
	private static function sanitize_amount(string $raw): string {
		$cleaned = preg_replace('/[^0-9.]/', '', $raw) ?? '';
		if ($cleaned === '' || CRToday_BigDecimal::of($cleaned === '' ? '0' : $cleaned)->isLessThanOrEqualTo(CRToday_BigDecimal::zero())) {
			return '1';
		}
		return $cleaned;
	}

	/**
	 * Convert an adjustment percent (e.g. +2.5 → "1.025") to the multiplier string.
	 */
	private static function adjustment_factor_string(float $percent): string {
		$factor    = 1 + ($percent / 100);
		$formatted = rtrim(rtrim(sprintf('%.8F', $factor), '0'), '.');
		return $formatted === '' ? '1' : $formatted;
	}

	/**
	 * Round a decimal string to $decimals using the configured rounding mode
	 * and format with locale-aware separators.
	 */
	private static function format_decimal(?string $value, int $decimals): string {
		if ($value === null) {
			return '—';
		}
		$rm      = self::resolve_rounding_mode();
		$rounded = (string) CRToday_BigDecimal::of($value)->toScale($decimals, $rm);
		return number_format_i18n((float) $rounded, $decimals);
	}

	/**
	 * Round a decimal string and return the raw (non-localised) string.
	 */
	private static function format_decimal_raw(?string $value, int $decimals): ?string {
		if ($value === null) {
			return null;
		}
		$rm = self::resolve_rounding_mode();
		return (string) CRToday_BigDecimal::of($value)->toScale($decimals, $rm);
	}

	/**
	 * Apply amount multiplication + percentage adjustment to a rate value.
	 */
	private static function adjust_value(?string $value, string $amount, float $adj_pct, int $decimals): ?string {
		if ($value === null) {
			return null;
		}
		if (self::bd_compare($amount, '1') !== 0) {
			$value = self::bd_multiply($value, $amount, $decimals);
		}
		if (self::has_adjustment($adj_pct)) {
			$value = self::bd_multiply($value, self::adjustment_factor_string($adj_pct), $decimals);
		}
		return $value;
	}
}
