<?php

namespace App\Domain\Training\Services;

use App\Domain\Training\Models\Holiday;
use App\Domain\Training\Models\TrainingGroup;
use Carbon\Carbon;
use Carbon\CarbonPeriod;

/**
 * How many training sessions a group actually holds between two dates.
 *
 * Counted from the timetable, not from the training_sessions table.
 * SessionGeneratorService only materialises rows about a week ahead, so
 * counting rows would under-report every question about the rest of the month —
 * which is exactly the question proration asks.
 *
 * The rules are the generator's, kept deliberately identical: a date counts
 * when an active schedule row names its weekday, the row is effective that day,
 * and no training-affecting holiday falls on it.
 */
class SessionCountService
{
    /**
     * Sessions the group holds in [$from, $to], both ends inclusive.
     */
    public function between(TrainingGroup $group, Carbon $from, Carbon $to): int
    {
        if ($to->lt($from)) {
            return 0;
        }

        $schedules = $group->schedules()->active()->get();

        if ($schedules->isEmpty()) {
            return 0;
        }

        $holidays = $this->holidays($group, $from, $to);

        $count = 0;

        // Per schedule row, not per date: a group that trains twice on the same
        // weekday — an early group and a late one — holds two sessions that day,
        // and a player joining owes for both.
        foreach ($schedules as $schedule) {
            foreach (CarbonPeriod::create($from->copy()->startOfDay(), $to->copy()->startOfDay()) as $date) {
                if ($date->dayOfWeek !== $schedule->day_of_week) {
                    continue;
                }

                if (isset($holidays[$date->toDateString()])) {
                    continue;
                }

                if ($schedule->effective_from && $date->lt($schedule->effective_from)) {
                    continue;
                }

                if ($schedule->effective_until && $date->gt($schedule->effective_until)) {
                    continue;
                }

                $count++;
            }
        }

        return $count;
    }

    /**
     * Sessions in the whole calendar month containing $date, and those still to
     * come on or after $date.
     *
     * Both figures come from the same walk so they cannot disagree — the price
     * of a session is the monthly fee over the first, and what is owed is that
     * times the second.
     *
     * @return array{total: int, remaining: int}
     */
    public function forMonth(TrainingGroup $group, Carbon $date): array
    {
        $monthStart = $date->copy()->startOfMonth();
        $monthEnd = $date->copy()->endOfMonth();

        return [
            'total' => $this->between($group, $monthStart, $monthEnd),
            'remaining' => $this->between($group, $date->copy()->startOfDay(), $monthEnd),
        ];
    }

    /**
     * Holiday dates that cancel training, keyed by Y-m-d for lookup.
     *
     * @return array<string, true>
     */
    private function holidays(TrainingGroup $group, Carbon $from, Carbon $to): array
    {
        $dates = Holiday::where('academy_id', $group->academy_id)
            ->where(function ($q) use ($group) {
                $q->whereNull('branch_id')->orWhere('branch_id', $group->branch_id);
            })
            ->affectsTraining()
            ->whereBetween('date', [$from->toDateString(), $to->toDateString()])
            ->pluck('date');

        $out = [];

        foreach ($dates as $date) {
            $out[$date instanceof \DateTimeInterface ? $date->format('Y-m-d') : (string) $date] = true;
        }

        return $out;
    }
}
