<?php

namespace App\Services\Billing;

/**
 * The arithmetic on an invoice, in cents, rounded once.
 *
 * Everything is integer cents from end to end. Money in floats accumulates a
 * cent here and there and then the sum printed under a column does not match
 * the column — which on an invoice is not a rounding artefact, it is a document
 * that does not add up.
 *
 * Rounding happens ONCE, at the point a figure is shown or stored, never per
 * line and then again on the total. A percentage discount applied line by line
 * and rounded each time drifts from the same discount applied to the sum, and
 * the customer's calculator is the one that will find it.
 *
 * Net is the basis for everything. Gross is derived and never the other way
 * round: taking VAT out of a gross figure and putting it back gives a different
 * number often enough to matter.
 */
final class InvoiceMath
{
    /** A discount or surcharge as a percentage of something. */
    public const PERCENT = 'percent';

    /** A discount or surcharge as a fixed amount in cents. */
    public const AMOUNT = 'amount';

    /**
     * One line's net total before any invoice-level adjustment.
     *
     * Quantity carries three decimals — hours, gigabytes and part-months are
     * all real — so it is held as thousandths and divided out at the end,
     * rather than multiplying a float by a price.
     */
    public static function lineNet(int $unitNetCents, int $quantityMilli): int
    {
        return (int) round($unitNetCents * $quantityMilli / 1000);
    }

    /**
     * A discount or surcharge, as a signed amount in cents.
     *
     * Positive raises the total, negative lowers it — so a discount is passed
     * as a negative value and a surcharge as a positive one, and nothing has to
     * remember which of two flags means which.
     *
     * @param  string  $type  PERCENT (value in hundredths of a percent) or
     *                        AMOUNT (value in cents)
     */
    public static function adjustment(int $baseCents, string $type, int $value): int
    {
        return $type === self::PERCENT
            ? (int) round($baseCents * $value / 10000)
            : $value;
    }

    /** VAT on a net figure. `$rateBasisPoints` is 2000 for 20 %. */
    public static function tax(int $netCents, int $rateBasisPoints): int
    {
        return (int) round($netCents * $rateBasisPoints / 10000);
    }

    /**
     * Every figure the document shows, from the lines and one adjustment.
     *
     * @param  array<int, array{unit_net_cents:int, quantity_milli:int, adjustment?:array{type:string, value:int}}>  $lines
     * @param  array{type:string, value:int}|null  $adjustment
     * @return array{subtotal:int, lines_net:int, adjustment:int, net:int, tax:int, gross:int}
     */
    public static function totals(array $lines, ?array $adjustment, int $rateBasisPoints): array
    {
        // The subtotal is the sum of the FULL line prices — the column the
        // reader can add up on the page. Discounts are collected separately and
        // shown as one figure beneath it.
        //
        // Printing each line already reduced was the previous attempt, and it
        // was worse: a line reading 202,50 next to "Rabatt 10 %" leaves the
        // reader unable to tell whether the discount is in that number or still
        // to come. Full price, then one subtraction, is a sum anybody can
        // follow.
        $subtotal = 0;
        $lineDiscounts = 0;

        foreach ($lines as $line) {
            $full = self::lineNet((int) $line['unit_net_cents'], (int) $line['quantity_milli']);
            $subtotal += $full;

            if (! empty($line['adjustment'])) {
                $lineDiscounts += self::adjustment(
                    $full,
                    (string) $line['adjustment']['type'],
                    (int) $line['adjustment']['value'],
                );
            }
        }

        // An adjustment on the whole document, on top of any on the lines.
        $wide = $adjustment === null
            ? 0
            : self::adjustment($subtotal, (string) $adjustment['type'], (int) $adjustment['value']);

        $adjusted = $lineDiscounts + $wide;
        $net = $subtotal + $adjusted;
        $tax = self::tax($net, $rateBasisPoints);

        return [
            'subtotal' => $subtotal,
            'lines_net' => $subtotal,   // kept: older snapshots read this name
            'adjustment' => $adjusted,
            'net' => $net,
            'tax' => $tax,
            'gross' => $net + $tax,
        ];
    }

    /** "1.234,56" — Austrian formatting, done here so no view invents its own. */
    public static function money(int $cents): string
    {
        return number_format($cents / 100, 2, ',', '.');
    }

    /** "12", "1,5", "0,25" — trailing zeroes dropped, because "12,000 Std." reads as a defect. */
    public static function quantity(int $quantityMilli): string
    {
        $formatted = number_format($quantityMilli / 1000, 3, ',', '.');

        return rtrim(rtrim($formatted, '0'), ',');
    }
}
