""" State Engine API - REST API für Begleiter-Modus. Endpoints: - GET /api/state/context - TeacherContext abrufen - GET /api/state/suggestions - Vorschläge abrufen - GET /api/state/dashboard - Dashboard-Daten - POST /api/state/milestone - Meilenstein abschließen - POST /api/state/transition - Phasen-Übergang """ import logging import uuid from datetime import datetime, timedelta from typing import Dict, Any, List from fastapi import APIRouter, HTTPException, Query from state_engine import ( AnticipationEngine, PhaseService, SchoolYearPhase, ClassSummary, Event, get_phase_info, ) from state_engine_models import ( MilestoneRequest, TransitionRequest, ContextResponse, SuggestionsResponse, DashboardResponse, _teacher_contexts, _milestones, get_or_create_context, update_context_from_services, get_phase_display_name, ) logger = logging.getLogger(__name__) router = APIRouter( prefix="/state", tags=["state-engine"], ) # Singleton instances _engine = AnticipationEngine() _phase_service = PhaseService() # ============================================================================ # API Endpoints # ============================================================================ @router.get("/context", response_model=ContextResponse) async def get_teacher_context(teacher_id: str = Query("demo-teacher")): """Gibt den aggregierten TeacherContext zurück.""" ctx = get_or_create_context(teacher_id) ctx = update_context_from_services(ctx) phase_info = get_phase_info(ctx.current_phase) return ContextResponse( context=ctx.to_dict(), phase_info={ "phase": phase_info.phase.value, "display_name": phase_info.display_name, "description": phase_info.description, "typical_months": phase_info.typical_months, "required_actions": phase_info.required_actions, "optional_actions": phase_info.optional_actions, } ) @router.get("/phase") async def get_current_phase(teacher_id: str = Query("demo-teacher")): """Gibt die aktuelle Phase mit Details zurück.""" ctx = get_or_create_context(teacher_id) phase_info = get_phase_info(ctx.current_phase) return { "current_phase": ctx.current_phase.value, "phase_info": { "display_name": phase_info.display_name, "description": phase_info.description, "expected_duration_weeks": phase_info.expected_duration_weeks, }, "days_in_phase": ctx.days_in_phase, "progress": _phase_service.get_progress_percentage(ctx), } @router.get("/phases") async def get_all_phases(): """Gibt alle Phasen mit Metadaten zurück.""" return { "phases": _phase_service.get_all_phases() } @router.get("/suggestions", response_model=SuggestionsResponse) async def get_suggestions(teacher_id: str = Query("demo-teacher")): """Gibt Vorschläge basierend auf dem aktuellen Kontext zurück.""" ctx = get_or_create_context(teacher_id) ctx = update_context_from_services(ctx) suggestions = _engine.get_suggestions(ctx) priority_counts = _engine.count_by_priority(ctx) return SuggestionsResponse( suggestions=[s.to_dict() for s in suggestions], current_phase=ctx.current_phase.value, phase_display_name=get_phase_display_name(ctx.current_phase.value), priority_counts=priority_counts, ) @router.get("/suggestions/top") async def get_top_suggestion(teacher_id: str = Query("demo-teacher")): """Gibt den wichtigsten einzelnen Vorschlag zurück.""" ctx = get_or_create_context(teacher_id) ctx = update_context_from_services(ctx) suggestion = _engine.get_top_suggestion(ctx) if not suggestion: return { "suggestion": None, "message": "Alles erledigt! Keine offenen Aufgaben." } return { "suggestion": suggestion.to_dict() } @router.get("/dashboard", response_model=DashboardResponse) async def get_dashboard_data(teacher_id: str = Query("demo-teacher")): """Gibt alle Daten für das Begleiter-Dashboard zurück.""" ctx = get_or_create_context(teacher_id) ctx = update_context_from_services(ctx) suggestions = _engine.get_suggestions(ctx) phase_info = get_phase_info(ctx.current_phase) required = set(phase_info.required_actions) completed = set(ctx.completed_milestones) completed_in_phase = len(required.intersection(completed)) all_phases = [] phase_order = [ SchoolYearPhase.ONBOARDING, SchoolYearPhase.SCHOOL_YEAR_START, SchoolYearPhase.TEACHING_SETUP, SchoolYearPhase.PERFORMANCE_1, SchoolYearPhase.SEMESTER_END, SchoolYearPhase.TEACHING_2, SchoolYearPhase.PERFORMANCE_2, SchoolYearPhase.YEAR_END, ] current_idx = phase_order.index(ctx.current_phase) if ctx.current_phase in phase_order else 0 for i, phase in enumerate(phase_order): info = get_phase_info(phase) all_phases.append({ "phase": phase.value, "display_name": info.display_name, "short_name": info.display_name[:10], "is_current": phase == ctx.current_phase, "is_completed": i < current_idx, "is_future": i > current_idx, }) return DashboardResponse( context={ "current_phase": ctx.current_phase.value, "phase_display_name": phase_info.display_name, "phase_description": phase_info.description, "weeks_since_start": ctx.weeks_since_start, "days_in_phase": ctx.days_in_phase, "federal_state": ctx.federal_state, "school_type": ctx.school_type, }, suggestions=[s.to_dict() for s in suggestions], stats={ "learning_units_created": ctx.stats.learning_units_created, "exams_scheduled": ctx.stats.exams_scheduled, "exams_graded": ctx.stats.exams_graded, "grades_entered": ctx.stats.grades_entered, "classes_count": len(ctx.classes), "students_count": ctx.total_students, }, upcoming_events=[ { "type": e.type, "title": e.title, "date": e.date.isoformat(), "in_days": e.in_days, "priority": e.priority, } for e in ctx.upcoming_events[:5] ], progress={ "completed": completed_in_phase, "total": len(required), "percentage": (completed_in_phase / len(required) * 100) if required else 100, "milestones_completed": list(completed.intersection(required)), "milestones_pending": list(required - completed), }, phases=all_phases, ) @router.post("/milestone") async def complete_milestone( request: MilestoneRequest, teacher_id: str = Query("demo-teacher") ): """Markiert einen Meilenstein als erledigt.""" milestone = request.milestone if teacher_id not in _milestones: _milestones[teacher_id] = [] if milestone not in _milestones[teacher_id]: _milestones[teacher_id].append(milestone) logger.info(f"Milestone '{milestone}' completed for teacher {teacher_id}") ctx = get_or_create_context(teacher_id) ctx.completed_milestones = _milestones[teacher_id] _teacher_contexts[teacher_id] = ctx new_phase = _phase_service.check_and_transition(ctx) if new_phase: ctx.current_phase = new_phase ctx.phase_entered_at = datetime.now() ctx.days_in_phase = 0 _teacher_contexts[teacher_id] = ctx logger.info(f"Auto-transitioned to {new_phase} for teacher {teacher_id}") return { "success": True, "milestone": milestone, "new_phase": new_phase.value if new_phase else None, "current_phase": ctx.current_phase.value, "completed_milestones": ctx.completed_milestones, } @router.post("/transition") async def transition_phase( request: TransitionRequest, teacher_id: str = Query("demo-teacher") ): """Führt einen manuellen Phasen-Übergang durch.""" try: target_phase = SchoolYearPhase(request.target_phase) except ValueError: raise HTTPException( status_code=400, detail=f"Ungültige Phase: {request.target_phase}" ) ctx = get_or_create_context(teacher_id) if not _phase_service.can_transition_to(ctx, target_phase): raise HTTPException( status_code=400, detail=f"Übergang von {ctx.current_phase.value} zu {target_phase.value} nicht erlaubt" ) old_phase = ctx.current_phase ctx.current_phase = target_phase ctx.phase_entered_at = datetime.now() ctx.days_in_phase = 0 _teacher_contexts[teacher_id] = ctx logger.info(f"Manual transition from {old_phase} to {target_phase} for teacher {teacher_id}") return { "success": True, "old_phase": old_phase.value, "new_phase": target_phase.value, "phase_info": get_phase_info(target_phase).__dict__, } @router.get("/next-phase") async def get_next_phase(teacher_id: str = Query("demo-teacher")): """Gibt die nächste Phase und Anforderungen zurück.""" ctx = get_or_create_context(teacher_id) next_phase = _phase_service.get_next_phase(ctx.current_phase) if not next_phase: return { "next_phase": None, "message": "Letzte Phase erreicht" } can_transition = _phase_service.can_transition_to(ctx, next_phase) next_info = get_phase_info(next_phase) current_info = get_phase_info(ctx.current_phase) missing = [ m for m in current_info.required_actions if m not in ctx.completed_milestones ] return { "current_phase": ctx.current_phase.value, "next_phase": next_phase.value, "next_phase_info": { "display_name": next_info.display_name, "description": next_info.description, }, "can_transition": can_transition, "missing_requirements": missing, } # ============================================================================ # Demo Data Endpoints (nur für Entwicklung) # ============================================================================ @router.post("/demo/add-class") async def demo_add_class( name: str = Query(...), grade_level: int = Query(...), student_count: int = Query(25), teacher_id: str = Query("demo-teacher") ): """Demo: Fügt eine Klasse zum Kontext hinzu.""" ctx = get_or_create_context(teacher_id) ctx.classes.append(ClassSummary( class_id=str(uuid.uuid4()), name=name, grade_level=grade_level, student_count=student_count, subject="Deutsch" )) ctx.total_students += student_count _teacher_contexts[teacher_id] = ctx return {"success": True, "classes": len(ctx.classes)} @router.post("/demo/add-event") async def demo_add_event( event_type: str = Query(...), title: str = Query(...), in_days: int = Query(...), teacher_id: str = Query("demo-teacher") ): """Demo: Fügt ein Event zum Kontext hinzu.""" ctx = get_or_create_context(teacher_id) ctx.upcoming_events.append(Event( type=event_type, title=title, date=datetime.now() + timedelta(days=in_days), in_days=in_days, priority="high" if in_days <= 3 else "medium" )) _teacher_contexts[teacher_id] = ctx return {"success": True, "events": len(ctx.upcoming_events)} @router.post("/demo/update-stats") async def demo_update_stats( learning_units: int = Query(0), exams_scheduled: int = Query(0), exams_graded: int = Query(0), grades_entered: int = Query(0), unanswered_messages: int = Query(0), teacher_id: str = Query("demo-teacher") ): """Demo: Aktualisiert Statistiken.""" ctx = get_or_create_context(teacher_id) if learning_units: ctx.stats.learning_units_created = learning_units if exams_scheduled: ctx.stats.exams_scheduled = exams_scheduled if exams_graded: ctx.stats.exams_graded = exams_graded if grades_entered: ctx.stats.grades_entered = grades_entered if unanswered_messages: ctx.stats.unanswered_messages = unanswered_messages _teacher_contexts[teacher_id] = ctx return {"success": True, "stats": ctx.stats.__dict__} @router.post("/demo/reset") async def demo_reset(teacher_id: str = Query("demo-teacher")): """Demo: Setzt den Kontext zurück.""" if teacher_id in _teacher_contexts: del _teacher_contexts[teacher_id] if teacher_id in _milestones: del _milestones[teacher_id] return {"success": True, "message": "Kontext zurückgesetzt"}