from typing import List, Optional, Literal from datetime import date, timedelta, datetime import uuid from domain.training.run import Run from domain.training.planned_session import PlannedSession from domain.runner.goal import Goal from domain.runner.profile import RunnerProfile def match_run_to_session(run: Run, sessions: List[PlannedSession]) -> Optional[uuid.UUID]: """ Matches a run to a planned session based on date and distance. Rules: - abs(run_date - planned_date) <= 1 day - run_distance >= 0.8 * target_distance - Match only one session (first match wins) """ run_date = run.start_time.date() run_dist_km = run.total_distance_m / 1000.0 for session in sessions: if session.completed_run_id is not None: continue date_diff = abs((run_date - session.planned_date).days) if date_diff <= 1 and run_dist_km >= (0.8 * session.target_distance_km): return session.id return None def classify_week(sessions: List[PlannedSession], current_volume: float, goal_volume: float) -> str: """ Classifies the week's structure based on completed sessions. Returns canonical string: strong_week, structured_but_light, rebuild_week, reset_week. """ if not sessions: return "reset_week" long_run_completed = any(s.session_type == "long_run" and s.completed_run_id for s in sessions) weekday_sessions = [s for s in sessions if s.session_type == "weekday"] weekdays_completed = sum(1 for s in weekday_sessions if s.completed_run_id) # Heuristics if long_run_completed: if weekdays_completed >= 2 or current_volume >= goal_volume: return "strong_week" else: return "structured_but_light" else: if weekdays_completed >= 2 or (current_volume > 0 and current_volume >= 0.5 * goal_volume): return "rebuild_week" else: return "reset_week" def generate_week_template( runner_id: uuid.UUID, week_start: date, goal: Optional[Goal], profile: Optional[RunnerProfile], previous_sessions: List[PlannedSession] = None, ) -> List[PlannedSession]: """ Generates a week template (list of PlannedSession). Logic: - Default 3 weekday + 1 long run - Long run distance based on goal (e.g. 30–40% weekly volume) - Weekday runs evenly distributed - Respect baseline weekly distance """ # 1. Determine Target Weekly Volume baseline = profile.baseline_weekly_km if (profile and profile.baseline_weekly_km is not None) else 20.0 target_volume = baseline if goal and goal.type == "volume" and goal.target_value is not None: target_volume = goal.target_value # Simple growth logic if previous week was successful (optional/not requested but good to have) # For now, stay simple as per requirements. # 2. Distribute Volume long_run_dist = target_volume * 0.35 weekday_total = target_volume - long_run_dist weekday_count = 3 weekday_dist = weekday_total / weekday_count # 3. Assign Dates # Mon (0), Tue (1), Wed (2), Thu (3), Fri (4), Sat (5), Sun (6) # We'll use Tue, Thu, Fri for weekdays and Sun for long run template_dates = { 1: ("weekday", weekday_dist), 3: ("weekday", weekday_dist), 4: ("weekday", weekday_dist), 6: ("long_run", long_run_dist), } sessions = [] for day_offset, (s_type, dist) in template_dates.items(): sessions.append( PlannedSession( runner_id=runner_id, week_start_date=week_start, session_type=s_type, planned_date=week_start + timedelta(days=day_offset), target_distance_km=round(dist, 1), ) ) return sessions