<?php

declare(strict_types=1);

namespace App\Service\Rtt;

use App\Dto\Rtt\WeekRecoveryDetail;
use App\Entity\Employee;
use App\Repository\EmployeeRttBalanceRepository;
use App\Repository\EmployeeRttPaymentRepository;
use App\State\EmployeeRttSummaryProvider;

/**
 * Computes the closing RTT balance of an exercise — the amount that must become the
 * opening report of the next exercise.
 *
 * Closing = opening report (N) + net earned (N) − RTT paid (N).
 *
 * This mirrors the "disponible" exposed by {@see EmployeeRttSummaryProvider}
 * (carry + currentYearRecovery − totalPaid), so the report carried to N+1 always equals
 * the balance the RTT tab displayed for N. The previous rollover only took the earned
 * minutes and dropped both the incoming report and the payments.
 */
final readonly class RttClosingBalanceService
{
    public function __construct(
        private RttRecoveryComputationService $recoveryService,
        private EmployeeRttBalanceRepository $balanceRepository,
        private EmployeeRttPaymentRepository $paymentRepository,
    ) {}

    public function computeClosingBalance(Employee $employee, int $exerciseYear): WeekRecoveryDetail
    {
        [$from, $to] = $this->recoveryService->resolveExerciseBounds($exerciseYear);
        $weeks       = $this->recoveryService->buildWeeksForExercise($from, $to);
        $weekRanges  = array_map(
            static fn (array $week): array => [
                'weekNumber' => (int) $week['weekNumber'],
                'start'      => $week['start'],
                'end'        => $week['end'],
            ],
            $weeks
        );

        // The exercise is fully closed at rollover time, so count every week up to its end.
        $byWeek = $this->recoveryService->computeRecoveryByWeek($employee, $weekRanges, $from, $to, $to);

        $orderedDetails = [];
        foreach ($weekRanges as $week) {
            $key              = $week['start']->format('Y-m-d');
            $orderedDetails[] = $byWeek[$key] ?? new WeekRecoveryDetail();
        }

        $opening  = $this->resolveOpeningReport($employee, $exerciseYear);
        $payments = $this->sumPayments($employee, $exerciseYear);

        return $this->fold($opening, $orderedDetails, $payments);
    }

    /**
     * Pure accumulation of the closing balance per bucket.
     *
     * Guarantees `sum(buckets) === opening.total + Σ week.total − payments.total`,
     * i.e. the carried report matches the displayed disponible regardless of how the
     * deficit cascade or the custom-recovery remainder is distributed across buckets.
     *
     * @param list<WeekRecoveryDetail> $weeks chronological order
     */
    public function fold(WeekRecoveryDetail $opening, array $weeks, WeekRecoveryDetail $payments): WeekRecoveryDetail
    {
        $b25  = $opening->base25Minutes;
        $bo25 = $opening->bonus25Minutes;
        $b50  = $opening->base50Minutes;
        $bo50 = $opening->bonus50Minutes;

        foreach ($weeks as $week) {
            if ($week->totalMinutes >= 0) {
                $b25 += $week->base25Minutes;
                $bo25 += $week->bonus25Minutes;
                $b50 += $week->base50Minutes;
                $bo50 += $week->bonus50Minutes;

                // Recovery not attributed to any 25/50 bucket (CUSTOM 1h=1h, rounding):
                // park it in the plain 25%-base bucket so the bucket sum keeps the total.
                $remainder = $week->totalMinutes
                    - ($week->base25Minutes + $week->bonus25Minutes + $week->base50Minutes + $week->bonus50Minutes);
                $b25 += $remainder;

                continue;
            }

            // Deficit week: drain the 50%-tier before the 25%-tier (mirrors
            // EmployeeRttSummaryProvider's cumulative cascade).
            $deficit          = -$week->totalMinutes;
            [$b50, $deficit]  = $this->consume($b50, $deficit);
            [$bo50, $deficit] = $this->consume($bo50, $deficit);
            [$b25, $deficit]  = $this->consume($b25, $deficit);
            $bo25 -= $deficit; // leftover may push the balance negative, as on screen
        }

        $b25 -= $payments->base25Minutes;
        $bo25 -= $payments->bonus25Minutes;
        $b50 -= $payments->base50Minutes;
        $bo50 -= $payments->bonus50Minutes;

        return new WeekRecoveryDetail(
            base25Minutes: $b25,
            bonus25Minutes: $bo25,
            base50Minutes: $b50,
            bonus50Minutes: $bo50,
            totalMinutes: $b25 + $bo25 + $b50 + $bo50,
        );
    }

    /**
     * The opening report of $year: the stored balance row when present, else the
     * dynamic fallback (earned in $year-1). Same resolution as
     * EmployeeRttSummaryProvider::resolveCarry.
     */
    private function resolveOpeningReport(Employee $employee, int $year): WeekRecoveryDetail
    {
        $balance = $this->balanceRepository->findOneByEmployeeAndYear($employee, $year);
        if (null !== $balance) {
            return new WeekRecoveryDetail(
                base25Minutes: $balance->getOpeningBase25Minutes(),
                bonus25Minutes: $balance->getOpeningBonus25Minutes(),
                base50Minutes: $balance->getOpeningBase50Minutes(),
                bonus50Minutes: $balance->getOpeningBonus50Minutes(),
                totalMinutes: $balance->getTotalOpeningMinutes(),
            );
        }

        return $this->recoveryService->computeTotalRecoveryForExercise($employee, $year - 1);
    }

    private function sumPayments(Employee $employee, int $year): WeekRecoveryDetail
    {
        $b25 = $bo25 = $b50 = $bo50 = 0;
        foreach ($this->paymentRepository->findByEmployeeAndYear($employee, $year) as $payment) {
            $b25 += $payment->getBase25Minutes();
            $bo25 += $payment->getBonus25Minutes();
            $b50 += $payment->getBase50Minutes();
            $bo50 += $payment->getBonus50Minutes();
        }

        return new WeekRecoveryDetail(
            base25Minutes: $b25,
            bonus25Minutes: $bo25,
            base50Minutes: $b50,
            bonus50Minutes: $bo50,
            totalMinutes: $b25 + $bo25 + $b50 + $bo50,
        );
    }

    /**
     * @return array{int, int} [remaining bucket, remaining deficit]
     */
    private function consume(int $bucket, int $deficit): array
    {
        $take = min($deficit, max(0, $bucket));

        return [$bucket - $take, $deficit - $take];
    }
}
