#!/usr/bin/env python3 from __future__ import annotations import argparse import os import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from dotenv import load_dotenv from garmin_coach_clone.auth import login from garmin_coach_clone.coach import ( best_workout_for_date, extract_training_plans, get_fbt_adaptive_workout, is_adaptive_plan, plan_id, task_workout_for_date, ) from garmin_coach_clone.dates import parse_date from garmin_coach_clone.io import dump_redacted_json, print_json from garmin_coach_clone.workouts import ( clone_workout_payload, existing_clone_names, summarize_workout, validate_workout_payload, ) load_dotenv() def main() -> int: parser = argparse.ArgumentParser( description="Clone a Garmin Coach workout if step data is exposed." ) parser.add_argument("--date", default="today", help="today, tomorrow, or YYYY-MM-DD") parser.add_argument("--dry-run", action="store_true", help="Convert and print JSON only.") parser.add_argument("--schedule", action="store_true", help="Upload and schedule the clone.") parser.add_argument( "--force", action="store_true", help="Create even if a clone already exists." ) parser.add_argument( "--dump-json", action="store_true", help="Dump redacted Coach/adaptive JSON." ) args = parser.parse_args() target_date = parse_date(args.date) prefix = os.getenv("CLONE_PREFIX", "GCClone") api = login() plans = extract_training_plans(api.get_training_plans()) candidates = [plan for plan in plans if is_adaptive_plan(plan)] if not candidates: print("No Garmin Coach/adaptive plan was identifiable. Cannot clone.") return 1 source = None source_plan_name = None for plan in candidates: pid = plan_id(plan) if pid is None: continue detail = ( api.get_adaptive_training_plan_by_id(pid) if str(plan.get("trainingPlanCategory", "")).upper() == "FBT_ADAPTIVE" else api.get_training_plan_by_id(pid) ) if args.dump_json: out = Path("debug") / f"coach_clone_source_{pid}_{target_date.isoformat()}.json" dump_redacted_json(out, detail) print(f"Redacted source JSON written to {out}") source = best_workout_for_date(detail, target_date) if source is None: task_workout = task_workout_for_date(detail, target_date) workout_uuid = task_workout.get("workoutUuid") if task_workout else None if workout_uuid: source = get_fbt_adaptive_workout(api, str(workout_uuid)) if source is not None: source_plan_name = plan.get("name") break if source is None: print( "Coach/adaptive plan data was fetched, but no object with workoutSegments " f"could be matched to {target_date.isoformat()}." ) print( "This means the missing piece is Coach step extraction, " "not normal workout scheduling." ) return 1 payload = clone_workout_payload(source, target_date, prefix) print(f"Source plan: {source_plan_name}") print(summarize_workout(payload)) validation_errors = validate_workout_payload(payload) if validation_errors: print("\nConverted clone payload failed local validation. Nothing was uploaded.") for error in validation_errors: print(f" - {error}") return 1 print("\nConverted clone payload passed local validation.") existing = existing_clone_names(api.get_workouts(limit=100), target_date, prefix) if existing and not args.force: print("A clone already exists for this date; skipping upload:") for name in existing: print(f" - {name}") return 0 if args.dry_run or not args.schedule: print("\nDry run: converted clone JSON follows. Nothing was uploaded.") print_json(payload) if not args.schedule: print("\nPass --schedule to upload and schedule this clone.") return 0 result = api.upload_workout(payload) workout_id = result.get("workoutId") or result.get("id") if workout_id is None: print("Upload returned no workoutId. Raw response:") print_json(result) return 1 schedule_result = api.schedule_workout(workout_id, target_date.isoformat()) print(f"Uploaded clone workout ID: {workout_id}") print("Schedule response:") print_json(schedule_result) print( f""" Next checks on the Edge 1030 after Garmin Connect sync: 1. Check Training > Workouts for "{payload["workoutName"]}". 2. Check Training > Training Plan > calendar icon > {target_date.isoformat()}. 3. Start it with a normal route/course active and verify navigation still works. 4. After completing, check whether Garmin Coach marks anything complete; this is not expected yet. """ ) return 0 if __name__ == "__main__": raise SystemExit(main())