<?php

namespace App\Services\Billing;

use App\Models\InvoiceSeries;
use Illuminate\Support\Facades\DB;

/**
 * Handing out invoice numbers, one at a time, without gaps.
 *
 * Gapless and ascending is a legal requirement in Austria, not a preference,
 * and it is the reason none of this is derived from the invoices table. Taking
 * "the highest number that exists plus one" hands the same number out twice the
 * moment two sales land together, and hands a used number back out after a row
 * is removed — a number that is already printed on a document somebody has.
 *
 * So the counter is a column, and it is read and advanced under a row lock
 * inside the caller's transaction. Two concurrent sales queue; neither gets the
 * other's number.
 */
final class InvoiceNumbers
{
    /**
     * Take the next number in a series.
     *
     * MUST be called inside a transaction — the lock is worthless otherwise,
     * and so is the invoice row that was supposed to consume the number.
     *
     * @return array{string, int, ?int} the formatted number, its sequence, its year
     */
    public function next(InvoiceSeries $series, ?int $year = null): array
    {
        if (! DB::transactionLevel()) {
            throw new \LogicException(
                'Invoice numbers must be taken inside a transaction: the number and the invoice have to commit together, or the series has a gap in it.'
            );
        }

        $year ??= (int) now()->format('Y');

        // lockForUpdate, not an atomic increment: the year check below has to
        // see a value nobody else can change between the read and the write.
        $locked = InvoiceSeries::query()
            ->whereKey($series->getKey())
            ->lockForUpdate()
            ->firstOrFail();

        $sequence = (int) $locked->next_number;
        $counterYear = $locked->counter_year;

        // A yearly series starts at 1 in a new year. Checked against the stored
        // year rather than against "is this January": an installation that
        // issued nothing for fourteen months must still restart, and one that
        // issues on New Year's Eve at 23:59 must not restart for the invoice
        // that follows a minute later under the old year.
        if ($locked->yearly_reset && $counterYear !== null && (int) $counterYear !== $year) {
            $sequence = 1;
        }

        $locked->forceFill([
            'next_number' => $sequence + 1,
            'counter_year' => $locked->yearly_reset ? $year : null,
        ])->save();

        return [$this->format($locked, $sequence, $year), $sequence, $locked->yearly_reset ? $year : null];
    }

    /** "RE-2026-0001", or "RE-0001" for a series that does not reset. */
    private function format(InvoiceSeries $series, int $sequence, int $year): string
    {
        $padded = str_pad((string) $sequence, (int) $series->digits, '0', STR_PAD_LEFT);

        return $series->yearly_reset
            ? sprintf('%s-%d-%s', $series->prefix, $year, $padded)
            : sprintf('%s-%s', $series->prefix, $padded);
    }
}
