"""Study streak and progress analytics.""" from __future__ import annotations from datetime import date, datetime, timezone from typing import Any from plane_mode_scholar.storage.sqlite_store import SQLiteStore def _parse_date(iso: str) -> datetime | None: if not iso: return None try: dt = datetime.fromisoformat(iso.replace("Z", "+00:00")) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt except ValueError: return None def compute_streak(sessions: list[dict[str, Any]]) -> int: """Count consecutive calendar days with completed sessions (UTC).""" days: set[str] = set() for s in sessions: if s.get("status") != "completed": continue dt = _parse_date(str(s.get("ended_at") or s.get("started_at", ""))) if dt: days.add(dt.date().isoformat()) if not days: return 0 today = datetime.now(timezone.utc).date() cursor = today streak = 0 while cursor.isoformat() in days: streak += 1 cursor = date.fromordinal(cursor.toordinal() - 1) return streak def compute_progress_stats( store: SQLiteStore, user_id: str, pack_id: str | None = None, ) -> dict[str, Any]: sessions = [ s.to_dict() for s in store.list_sessions(pack_id=pack_id, user_id=user_id, limit=50) ] completed = [s for s in sessions if s.get("status") == "completed"] active_memories = store.list_memories(pack_id=pack_id, user_id=user_id, status="active") quiz_correct = sum( 1 for m in active_memories if "quiz" in m.tags and m.type.value == "mastery_estimate" ) return { "streak_days": compute_streak(sessions), "sessions_total": len(sessions), "sessions_completed": len(completed), "active_memories": len(active_memories), "last_session_date": ( str(completed[0].get("ended_at", ""))[:10] if completed else None ), }