import 'package:flutter/material.dart'; import '../models/user_profile.dart'; import '../models/cycle_entry.dart'; class CycleInfo { final CyclePhase phase; final int dayOfCycle; final int daysUntilPeriod; final bool isPeriodExpected; CycleInfo({ required this.phase, required this.dayOfCycle, required this.daysUntilPeriod, required this.isPeriodExpected, }); @override bool operator ==(Object other) => identical(this, other) || other is CycleInfo && runtimeType == other.runtimeType && phase == other.phase && dayOfCycle == other.dayOfCycle && daysUntilPeriod == other.daysUntilPeriod && isPeriodExpected == other.isPeriodExpected; @override int get hashCode => phase.hashCode ^ dayOfCycle.hashCode ^ daysUntilPeriod.hashCode ^ isPeriodExpected.hashCode; } class CycleService { /// Calculates the current cycle information based on user profile /// Calculates the current cycle information based on user profile and cycle entries static CycleInfo calculateCycleInfo( UserProfile? user, List entries) { if (user == null) { return CycleInfo( phase: CyclePhase.follicular, dayOfCycle: 1, daysUntilPeriod: 28, isPeriodExpected: false, ); } DateTime? lastPeriodStart = user.lastPeriodStartDate; // Find the most recent period start from entries if available and more recent // We look for a sequence of period days and take the first one if (entries.isNotEmpty) { final sortedEntries = List.from(entries) ..sort((a, b) => b.date.compareTo(a.date)); for (var entry in sortedEntries) { if (entry.isPeriodDay) { // Check if this is a "start" of a period (previous day was not period or no entry) // Simplified logic: Just take the most recent period day found and assume it's part of the current/last period // A better approach for "Day 1" is to find the First day of the contiguous block. // However, for basic calculation, if we find a period day at Date X, // and today is Date Y. // If X is very recent, we are in the period. // Correct logic: Identify the START DATE of the last period group. // 1. Find the latest period entry. // 2. Look backwards from there as long as there are consecutive period days. // Check if we have a period day "tomorrow" relative to this entry? No, we are iterating backwards (descending). // So if we found a period day, we need to check if the NEXT entry (which is earlier in time) is also a period day. // If so, that earlier day is the better candidate for "Start". // Let's iterate linearly. // Since we sorted DESC, `entry` is the LATEST period day. // We need to see if there are consecutive period days before it. // But wait, the user might have logged Day 1, Day 2, Day 3. // `entry` will be Day 3. // We want Day 1. // Let's try a different approach: // Get all period days sorted DESC. final periodDays = sortedEntries.where((e) => e.isPeriodDay).toList(); if (periodDays.isNotEmpty) { // Take the latest block DateTime latestParams = periodDays.first.date; // Now find the "start" of this block // We iterate backwards from the *latest* date found DateTime currentSearch = latestParams; DateTime startOfBlock = latestParams; // Check if we have an entry for the day before bool foundPrevious = true; while (foundPrevious) { final dayBefore = currentSearch.subtract(const Duration(days: 1)); final hasDayBefore = periodDays.any((e) => DateUtils.isSameDay(e.date, dayBefore)); if (hasDayBefore) { currentSearch = dayBefore; startOfBlock = dayBefore; } else { foundPrevious = false; } } // If this calculated start is more recent than the user profile one, use it if (lastPeriodStart == null || startOfBlock.isAfter(lastPeriodStart)) { lastPeriodStart = startOfBlock; } } break; // We only care about the most recent period block } } } if (lastPeriodStart == null) { return CycleInfo( phase: CyclePhase.follicular, dayOfCycle: 1, daysUntilPeriod: user.averageCycleLength, isPeriodExpected: false, ); } // Check if the calculated last period is in the future (invalid state validation) if (lastPeriodStart.isAfter(DateTime.now())) { // Fallback to today if data is weird, or just use it (maybe user logged future?) // Let's stick to standard logic: } final cycleLength = user.averageCycleLength; final now = DateTime.now(); // Normalize dates to midnight for accurate day counting final startOfToday = DateTime(now.year, now.month, now.day); final startOfCycle = DateTime( lastPeriodStart.year, lastPeriodStart.month, lastPeriodStart.day); final daysSinceLastPeriod = startOfToday.difference(startOfCycle).inDays + 1; // If negative (future date), handle gracefully if (daysSinceLastPeriod < 1) { return CycleInfo( phase: CyclePhase.follicular, dayOfCycle: 1, daysUntilPeriod: cycleLength, isPeriodExpected: false, ); } // Handle cases where last period was long ago (more than one cycle) final int calculatedDayOfCycle = ((daysSinceLastPeriod - 1) % cycleLength) + 1; // Check if we are in the predicted menstrual phase but no period is logged bool isPeriodLoggedToday = entries.any((e) => DateUtils.isSameDay(e.date, now) && e.isPeriodDay); CyclePhase phase; int dayOfCycle = calculatedDayOfCycle; if (calculatedDayOfCycle <= user.averagePeriodLength) { if (isPeriodLoggedToday) { phase = CyclePhase.menstrual; } else { // No period logged today, but we are in the predicted window. // Stay in Luteal and extend the day count. phase = CyclePhase.luteal; dayOfCycle = daysSinceLastPeriod; } } else if (calculatedDayOfCycle <= 13) { phase = CyclePhase.follicular; } else if (calculatedDayOfCycle <= 16) { phase = CyclePhase.ovulation; } else { phase = CyclePhase.luteal; } final daysUntilPeriod = dayOfCycle >= cycleLength ? 0 : cycleLength - dayOfCycle; return CycleInfo( phase: phase, dayOfCycle: dayOfCycle, daysUntilPeriod: daysUntilPeriod, isPeriodExpected: daysUntilPeriod <= 0 || calculatedDayOfCycle <= 5, ); } /// Calculates the cycle phase for a specific date (past or future) static CyclePhase? getPhaseForDate(DateTime date, UserProfile? user) { if (user == null || user.lastPeriodStartDate == null) return null; final lastPeriodStart = user.lastPeriodStartDate!; // Normalize dates final checkDate = DateTime(date.year, date.month, date.day); final startCycle = DateTime( lastPeriodStart.year, lastPeriodStart.month, lastPeriodStart.day); final daysDifference = checkDate.difference(startCycle).inDays; // If date is before the last known period, we can't reliably predict using this simple logic // (though in reality we could project backwards, but let's stick to forward/current) if (daysDifference < 0) return null; final cycleLength = user.averageCycleLength; final dayOfCycle = (daysDifference % cycleLength) + 1; if (dayOfCycle <= user.averagePeriodLength) return CyclePhase.menstrual; if (dayOfCycle <= 13) return CyclePhase.follicular; if (dayOfCycle <= 16) return CyclePhase.ovulation; return CyclePhase.luteal; } /// Predicts period days for the next [months] months static List predictNextPeriodDays(UserProfile? user, {int months = 12}) { if (user == null || user.lastPeriodStartDate == null) return []; final predictedDays = []; final lastPeriodStart = user.lastPeriodStartDate!; final cycleLength = user.averageCycleLength; final periodLength = user.averagePeriodLength; // Start predicting from the NEXT cycle if the current one is finished, // or just project out from the last start date. // We want to list all future period days. DateTime currentCycleStart = lastPeriodStart; // Project forward for roughly 'months' months // A safe upper bound for loop is months * 30 days final limitDate = DateTime.now().add(Duration(days: months * 30)); while (currentCycleStart.isBefore(limitDate)) { // Add period days for this cycle for (int i = 0; i < periodLength; i++) { final periodDay = currentCycleStart.add(Duration(days: i)); if (periodDay.isAfter(DateTime.now())) { predictedDays.add(periodDay); } } // Move to next cycle currentCycleStart = currentCycleStart.add(Duration(days: cycleLength)); } return predictedDays; } /// Format cycle day for display static String getDayOfCycleDisplay(int day) => 'Day $day'; /// Get phase description static String getPhaseDescription(CyclePhase phase) { switch (phase) { case CyclePhase.menstrual: return 'Your body is resting and clearing. Be gentle with yourself.'; case CyclePhase.follicular: return 'Energy and optimism are rising. A great time for new projects.'; case CyclePhase.ovulation: return 'You are at your peak energy and fertility.'; case CyclePhase.luteal: return 'Progesterone is rising. You may feel more introverted or sensitive.'; } } }