<?php

namespace App\Services\Billing;

use App\Models\Subscription;
use Illuminate\Support\Carbon;

/**
 * The fourteen-day right of withdrawal, and what it costs when it is used.
 *
 * EU distance selling. In Austria the FAGG, in Germany §312g BGB, and the same
 * substance in every member state: a CONSUMER who concludes a contract at a
 * distance may withdraw from it within fourteen days without giving a reason,
 * and gets their money back. A business customer has no such right at all —
 * which is why nothing here will answer a question about a contract whose
 * customer is a business, and why `customers.customer_type` had to exist before
 * any of this could be written.
 *
 * ## It IS simply "give it all back"
 *
 * FAGG §16 would let us keep the pro-rata value of the days the cloud actually
 * ran, where the consumer expressly asked for the service to begin inside the
 * window and was told what that would cost them. The owner has decided not to:
 * a consumer who withdraws gets the WHOLE amount back, and a cancellation
 * invoice is issued for the whole of it.
 *
 * That is a business decision and not a legal one, and it is more generous than
 * the statute requires — which is why nothing turns on the express-start consent
 * any more. It used to be the hinge the refund swung on; a full refund makes it
 * irrelevant, and `immediate_start_consent_at` on the order and on the contract
 * is now a record of what the customer ticked rather than a gate anything reads.
 *
 * ## Days are still counted
 *
 * By whole days, over the term the customer actually paid for, and a started day
 * counts as delivered. Nothing is charged for them, but how long the service ran
 * before a withdrawal is a fact about the contract, it goes into the proof
 * register, and an operator recording a withdrawal by telephone is shown it.
 */
final readonly class WithdrawalRight
{
    /** Fourteen days, from the conclusion of the contract. The law's number. */
    public const WINDOW_DAYS = 14;

    private function __construct(
        /** Does this contract have a withdrawal right at all — is the customer a consumer? */
        public bool $applies,
        /** When the window opened: the moment the contract was concluded. */
        public ?Carbon $opensAt,
        /** When it closes. Stamped on the contract, not recomputed here. */
        public ?Carbon $endsAt,
        /** May it be exercised right now? */
        public bool $open,
        /** Why not, in the sentence the customer is shown. Null while it is open. */
        public ?string $refusal,
    ) {}

    public static function for(?Subscription $subscription): self
    {
        if ($subscription === null) {
            return new self(false, null, null, false, __('withdrawal.refusal_no_contract'));
        }

        $opensAt = $subscription->withdrawalOpenedAt();
        $endsAt = $subscription->withdrawal_ends_at
            // A contract opened before the column existed still had a window;
            // it is derived here rather than left null so an old contract reads
            // as "expired" instead of "no window was ever counted".
            ?? $opensAt?->copy()->addDays(self::WINDOW_DAYS);

        // Asked of the CUSTOMER, never of the VAT number. A customer nobody has
        // asked is a consumer here — see Customer::isConsumer() for why the
        // unknown case leans this way and not the other.
        if ($subscription->customer?->isBusiness() === true) {
            return new self(false, $opensAt, $endsAt, false, __('withdrawal.refusal_business'));
        }

        if ($subscription->isWithdrawn()) {
            return new self(true, $opensAt, $endsAt, false, __('withdrawal.refusal_already'));
        }

        if ($endsAt === null) {
            // A contract with no beginning on record. Refused rather than
            // guessed: inventing the moment a statutory deadline started is the
            // one thing this class must never do.
            return new self(true, $opensAt, null, false, __('withdrawal.refusal_no_contract'));
        }

        return $endsAt->isFuture()
            ? new self(true, $opensAt, $endsAt, true, null)
            : new self(true, $opensAt, $endsAt, false, __('withdrawal.refusal_expired'));
    }

    /**
     * How many whole days of the window are left, for the sentence that says so.
     *
     * Rounded UP, because a customer told "1 Tag" on the last afternoon still
     * has that afternoon, and rounding down would tell them the window had shut
     * while it was open.
     */
    public function daysLeft(?Carbon $at = null): int
    {
        if (! $this->open || $this->endsAt === null) {
            return 0;
        }

        return max(1, (int) ceil(($at ?? now())->diffInSeconds($this->endsAt, absolute: false) / 86400));
    }

    /**
     * The whole days the term the customer paid for is made of.
     *
     * From the contract's own boundaries, so a month is 28, 30 or 31 days and a
     * year is 365 or 366. Nothing is charged against it any more — it is what
     * `deliveredDays()` is a share OF, in the register and on the operator's
     * screen.
     */
    public static function termDays(Subscription $subscription): int
    {
        $from = $subscription->current_period_start ?? $subscription->started_at;
        $to = $subscription->current_period_end;

        if ($from === null || $to === null || ! $to->greaterThan($from)) {
            // Nothing sensible to divide by. One day means the first day of
            // service is the whole term, which charges the consumer the full
            // amount — so this branch is only ever reached by a contract that is
            // already broken, and it fails towards "we keep what we were paid"
            // rather than towards a division by zero.
            return 1;
        }

        return max(1, (int) ceil($from->diffInSeconds($to, absolute: true) / 86400));
    }

    /**
     * The days of service actually delivered by the moment of withdrawal.
     *
     * A started day counts, so this is never zero: a consumer who withdraws
     * three hours after buying still had a running cloud for those three hours.
     * Capped at the term, because no more of a term can be delivered than it
     * holds.
     */
    public static function deliveredDays(Subscription $subscription, ?Carbon $at = null): int
    {
        $from = $subscription->current_period_start ?? $subscription->started_at;
        $at ??= now();

        if ($from === null || ! $at->greaterThan($from)) {
            return 1;
        }

        $days = (int) ceil($from->diffInSeconds($at, absolute: true) / 86400);

        return max(1, min($days, self::termDays($subscription)));
    }
}
