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. DateTime potentialStart = entry.date; // 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 dayOfCycle = ((daysSinceLastPeriod - 1) % cycleLength) + 1; final daysUntilPeriod = cycleLength - dayOfCycle; CyclePhase phase; if (dayOfCycle <= user.averagePeriodLength) { // Use variable period length phase = CyclePhase.menstrual; } else if (dayOfCycle <= 13) { phase = CyclePhase.follicular; } else if (dayOfCycle <= 16) { phase = CyclePhase.ovulation; } else { phase = CyclePhase.luteal; } return CycleInfo( phase: phase, dayOfCycle: dayOfCycle, daysUntilPeriod: daysUntilPeriod, isPeriodExpected: daysUntilPeriod <= 0 || dayOfCycle <= 5, ); } /// 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.'; } } }