from __future__ import annotations import asyncio import logging from datetime import datetime, timedelta from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.date import DateTrigger from server.ai import generate_daily_motivation, generate_oracle from server.config_loader import load_meds_config, slot_to_dict, today_str from server.db import get_db, utc_now_iso from server.messages import pick from server.push import send_slot_reminder from server.slots import build_today, get_log_for_day, mark_missed_for_overdue logger = logging.getLogger(__name__) scheduler = AsyncIOScheduler() async def _remind_slot(slot_id: str) -> None: config = load_meds_config() tz = config.timezone day = today_str(tz) db = await get_db() try: logs = await get_log_for_day(db, day) if slot_id in logs and logs[slot_id]["status"] == "taken": return slot = next((s for s in config.slots if s.id == slot_id), None) if not slot: return title = f"{slot.label} — Med-Time!" body = pick("reminder", label=slot.label, med=slot.meds[0].name if slot.meds else "Medis") await send_slot_reminder(db, slot_id, title, body) finally: await db.close() async def _generate_daily_motivation() -> None: config = load_meds_config() db = await get_db() try: await generate_daily_motivation(db, config.timezone) except Exception: logger.exception("Daily motivation generation failed") finally: await db.close() async def _generate_weekly_oracle() -> None: config = load_meds_config() db = await get_db() try: await generate_oracle(db, config.timezone) except Exception: logger.exception("Weekly oracle generation failed") finally: await db.close() async def _evening_check() -> None: db = await get_db() try: marked = await mark_missed_for_overdue(db) for slot_id in marked: body = pick("missed") await send_slot_reminder(db, slot_id, "Verpasst?", body) finally: await db.close() async def schedule_snooze(slot_id: str, minutes: int) -> str: config = load_meds_config() tz = config.timezone now = datetime.now(tz) until = now + timedelta(minutes=minutes) day = today_str(tz) db = await get_db() try: await db.execute( """ INSERT INTO snooze (slot_id, day, snooze_until) VALUES (?, ?, ?) ON CONFLICT(slot_id, day) DO UPDATE SET snooze_until=excluded.snooze_until """, (slot_id, day, until.isoformat()), ) await db.commit() finally: await db.close() run_at = until.astimezone(tz).replace(tzinfo=None) scheduler.add_job( _remind_slot, trigger=DateTrigger(run_date=run_at), args=[slot_id], id=f"snooze-{slot_id}-{until.timestamp()}", replace_existing=False, ) return until.isoformat() def start_scheduler() -> None: config = load_meds_config() tz = config.timezone for slot in config.slots: h, m = slot.time.hour, slot.time.minute scheduler.add_job( _remind_slot, trigger=CronTrigger(hour=h, minute=m, timezone=tz), args=[slot.id], id=f"remind-{slot.id}", replace_existing=True, ) scheduler.add_job( _evening_check, trigger=CronTrigger(hour=21, minute=0, timezone=tz), id="evening-check", replace_existing=True, ) scheduler.add_job( _generate_daily_motivation, trigger=CronTrigger(hour=3, minute=0, timezone=tz), id="daily-motivation", replace_existing=True, ) scheduler.add_job( _generate_weekly_oracle, trigger=CronTrigger(day_of_week="mon", hour=3, minute=5, timezone=tz), id="weekly-oracle", replace_existing=True, ) if not scheduler.running: scheduler.start() logger.info("Scheduler started for timezone %s", tz)