<?php

namespace App\Support;

/**
 * Between what an operator types and what the catalogue stores.
 *
 * Prices are stored in cents — integers, because money in floats eventually
 * bills someone 178.99999999. But the console asked the owner to type cents
 * too, so a €799 plan was entered as 79900, and a slipped digit is a factor of
 * ten on an invoice. The form takes euros; the conversion happens here, once.
 *
 * Parsing is done on the STRING, not by multiplying a float: (float) '79.90'
 * times 100 is 7989.999… on a binary float, and casting that to int truncates
 * to 7989. One cent, every time, on exactly the prices people actually charge.
 */
final class Money
{
    /** What plan_prices.amount_cents can actually hold (unsigned int). */
    public const MAX_CENTS = 4294967295;

    /**
     * Euros as typed → cents, or null when it is not a price at all.
     *
     * Accepts both separators: a German keyboard produces "79,90" and a copied
     * figure produces "79.90", and rejecting either would just be rude.
     * Thousands separators are refused rather than guessed — "1.000" is a
     * thousand to one reader and one euro to another.
     */
    public static function toCents(string $euros): ?int
    {
        $value = str_replace(',', '.', trim($euros));

        if (! preg_match('/^\d{1,9}(\.\d{1,2})?$/', $value)) {
            return null;
        }

        [$whole, $fraction] = array_pad(explode('.', $value, 2), 2, '');

        $cents = (int) $whole * 100 + (int) str_pad($fraction, 2, '0');

        // The column is an unsigned int. Nine whole digits parse happily and
        // then overflow it, turning a typo into a database error on submit
        // instead of a message next to the field.
        return $cents > self::MAX_CENTS ? null : $cents;
    }

    /** Cents → the string the form shows, with a decimal comma. */
    public static function fromCents(int $cents): string
    {
        return number_format($cents / 100, 2, ',', '');
    }
}
