<?php

namespace Functional\Streaks\Services;

use Carbon\Carbon;
use Carbon\CarbonInterface;
use Functional\Streaks\Enums\StreakDaySource;
use Functional\Streaks\Models\Achievement;
use Functional\Streaks\Models\Streak;
use Functional\Streaks\Models\StreakDay;
use Functional\Users\Models\User;

/**
 * Cœur de la gamification StreakFit : enregistre une activité et recalcule le streak courant du user.
 */
class StreakService
{
    /** Paliers d'achievements débloqués selon le streak courant atteint. */
    private const MILESTONES = [
        'first_workout' => 1,
        'streak_3' => 3,
        'streak_7' => 7,
        'streak_30' => 30,
        'streak_100' => 100,
    ];

    /**
     * Enregistre une activité pour une date donnée et recalcule le streak du user.
     * Idempotent : rejouer la même date ne crée pas de doublon et ne modifie pas le compteur.
     */
    public function recordActivity(
        User $user,
        CarbonInterface $date,
        string $source = 'workout',
        ?int $workoutSessionId = null,
    ): Streak {
        // Comparaisons de dates uniquement (pas d'heure) : on travaille sur un jour calendaire.
        $activityDate = Carbon::parse($date)->startOfDay();

        // Un seul streak_day par (user, date) : no-op si déjà enregistré (unique côté migration).
        StreakDay::firstOrCreate(
            ['user_id' => $user->id, 'date' => $activityDate->toDateString()],
            ['source' => StreakDaySource::from($source), 'workout_session_id' => $workoutSessionId],
        );

        $streak = Streak::firstOrCreate(['user_id' => $user->id]);

        $streak->current_count = $this->nextCurrentCount($streak, $activityDate);
        $streak->longest_count = max($streak->longest_count, $streak->current_count);
        $streak->last_active_date = $activityDate->toDateString();
        $streak->save();

        $this->awardMilestones($user, $streak);

        return $streak;
    }

    /**
     * Calcule le nouveau compteur courant en comparant last_active_date et la date d'activité,
     * au jour près (Y-m-d), pas en datetime.
     */
    private function nextCurrentCount(Streak $streak, Carbon $activityDate): int
    {
        if (! $streak->last_active_date) {
            return 1;
        }

        $lastActiveDate = Carbon::parse($streak->last_active_date)->startOfDay();
        $yesterday = $activityDate->copy()->subDay();

        // Jour consécutif : le streak continue.
        if ($lastActiveDate->isSameDay($yesterday)) {
            return $streak->current_count + 1;
        }

        // Déjà comptée aujourd'hui : rejouer l'événement ne change rien.
        if ($lastActiveDate->isSameDay($activityDate)) {
            return $streak->current_count;
        }

        // Trou dans le streak : un freeze ne couvre qu'un unique jour manqué (écart de 2 jours).
        $gapInDays = $lastActiveDate->diffInDays($activityDate);

        if ($streak->freezes_available > 0 && $gapInDays === 2) {
            $streak->freezes_available -= 1;

            return $streak->current_count + 1;
        }

        // Trou trop grand (ou pas de freeze disponible) : le streak repart de zéro.
        return 1;
    }

    /** Débloque les achievements dont le palier est atteint (idempotent via unique user+key). */
    private function awardMilestones(User $user, Streak $streak): void
    {
        foreach (self::MILESTONES as $key => $threshold) {
            if ($streak->current_count < $threshold) {
                continue;
            }

            Achievement::firstOrCreate(
                ['user_id' => $user->id, 'key' => $key],
                ['earned_at' => now()],
            );
        }
    }
}
