<?php

namespace App\Domain\Shared\Services;

use App\Domain\Shared\DTOs\ProrationResult;
use App\Domain\Shared\Enums\ProrationMode;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Services\SessionCountService;
use Carbon\Carbon;

/**
 * What someone joining part-way through a month pays.
 *
 * Three answers are offered, and the desk picks one per registration: the full
 * month, half of it, or only the sessions left in the month. The last is the
 * default and replaced an older rule that charged for remaining *days* on a
 * hardcoded 30-day month — a unit the academy never sold. A programme training
 * Sunday and Tuesday holds no sessions over a long weekend, so a player joining
 * on the 22nd was billed a third of a month for perhaps two trainings.
 */
class ProrationService
{
    public function __construct(
        private readonly SettingsService $settings,
        private readonly SessionCountService $sessions,
    ) {}

    public function isEnabled(): bool
    {
        return (bool) $this->settings->get('enrollment.allow_proration', false);
    }

    public function renewalDay(): int
    {
        return (int) $this->settings->get('enrollment.renewal_day', 1);
    }

    /**
     * What the desk gets before it chooses. Full month when proration is off —
     * the other two are not offered at all in that case.
     */
    public function defaultMode(): ProrationMode
    {
        return $this->isEnabled() ? ProrationMode::RemainingSessions : ProrationMode::FullMonth;
    }

    /**
     * @param  int  $baseAmount  the full monthly fee, in piasters
     * @param  TrainingGroup|null  $group  whose timetable says how many sessions the month holds
     */
    public function calculate(
        int $baseAmount,
        ?Carbon $enrollmentDate = null,
        ?TrainingGroup $group = null,
        ProrationMode|string|null $mode = null,
    ): ProrationResult {
        $date = $enrollmentDate ?? now();
        $renewalDay = $this->renewalDay();

        $mode = is_string($mode) ? ProrationMode::tryFrom($mode) : $mode;
        $mode ??= $this->defaultMode();

        // The setting is the gate. A mode posted from a browser cannot switch
        // proration on for an academy that turned it off.
        if (! $this->isEnabled()) {
            $mode = ProrationMode::FullMonth;
        }

        return match ($mode) {
            ProrationMode::FullMonth => ProrationResult::fullMonth($baseAmount, $renewalDay),
            ProrationMode::HalfMonth => $this->halfMonth($baseAmount, $renewalDay),
            ProrationMode::RemainingSessions => $this->remainingSessions($baseAmount, $date, $group, $renewalDay),
        };
    }

    private function halfMonth(int $baseAmount, int $renewalDay): ProrationResult
    {
        // Rounded up, like every other partial charge here: the academy does
        // not lose a piaster to a division, and the player is never surprised
        // by more than one.
        $amount = (int) ceil($baseAmount / 2);

        return new ProrationResult(
            applied: true,
            originalAmount: $baseAmount,
            proratedAmount: $amount,
            mode: ProrationMode::HalfMonth,
            renewalDay: $renewalDay,
            description: 'متناسب: نصف شهر',
        );
    }

    private function remainingSessions(
        int $baseAmount,
        Carbon $date,
        ?TrainingGroup $group,
        int $renewalDay,
    ): ProrationResult {
        // No group, or a group with no timetable, cannot answer "how many
        // trainings are left". Charging the full month is the conservative
        // answer and says so, rather than blocking a registration over a
        // programme whose schedule nobody has filled in yet.
        if (! $group) {
            return ProrationResult::fullMonth($baseAmount, $renewalDay, 'لا توجد مجموعة لحساب التمرينات — شهر كامل');
        }

        ['total' => $total, 'remaining' => $remaining] = $this->sessions->forMonth($group, $date);

        if ($total <= 0) {
            return ProrationResult::fullMonth($baseAmount, $renewalDay, 'لا يوجد جدول تمرين — شهر كامل');
        }

        // Joining before the month's first session buys the whole month; there
        // is nothing to discount.
        if ($remaining >= $total) {
            return new ProrationResult(
                applied: false,
                originalAmount: $baseAmount,
                proratedAmount: $baseAmount,
                mode: ProrationMode::RemainingSessions,
                renewalDay: $renewalDay,
                description: '',
                totalSessions: $total,
                remainingSessions: $remaining,
            );
        }

        $amount = (int) ceil($baseAmount * $remaining / $total);

        return new ProrationResult(
            applied: true,
            originalAmount: $baseAmount,
            proratedAmount: $amount,
            mode: ProrationMode::RemainingSessions,
            renewalDay: $renewalDay,
            // The Arabic word "متناسب" is load-bearing: ParticipantBillingService
            // reads it off the invoice line to recognise a prorated subscription,
            // there being no column that records the fact.
            description: "متناسب: {$remaining} من {$total} تمرين",
            totalSessions: $total,
            remainingSessions: $remaining,
        );
    }
}
