#!/usr/bin/env python3
"""Deadline alert loop. Reads an assignments JSON file and sends a Telegram
message for each item that is overdue, due today, or due tomorrow.
Deduplicates with a 24h cache, max 3 alerts per run. Run it from cron/systemd.

Input file format:
    {"assignments": [{"title": str, "course": str,
                      "due_iso": ISO-8601 str, "due_unix": epoch int}]}

Config via env vars: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID,
ASSIGNMENTS_FILE, CACHE_FILE, ALERT_TZ.

Usage:
    python3 deadline-alerts.py          # run normally
    python3 deadline-alerts.py --test   # dry run, print only, no Telegram
"""

import argparse
import hashlib
import json
import os
import sys
import urllib.error
import urllib.request
from datetime import datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo

LOCAL_TZ = ZoneInfo(os.environ.get("ALERT_TZ", "UTC"))
ASSIGNMENTS_FILE = Path(os.environ.get("ASSIGNMENTS_FILE", "assignments.json"))
CACHE_FILE = Path(os.environ.get("CACHE_FILE", ".alerts_sent.json"))
TELEGRAM_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT = os.environ.get("TELEGRAM_CHAT_ID", "")

QUIET_START = 23   # no non-overdue alerts from this hour...
QUIET_END = 7      # ...until this hour
DEDUP_WINDOW = 24 * 3600  # seconds
MAX_ALERTS = 3

# Optional: map long course names to short labels, e.g. {"Intro to Biology": "Bio"}
COURSE_SHORT = {}


def load_assignments(now):
    if not ASSIGNMENTS_FILE.exists():
        return []
    data = json.loads(ASSIGNMENTS_FILE.read_text())
    now_unix = now.timestamp()
    today_str = now.strftime("%Y-%m-%d")
    tomorrow_str = (now + timedelta(days=1)).strftime("%Y-%m-%d")

    results = []
    for a in data.get("assignments", []):
        due_iso = a.get("due_iso", "")
        if not due_iso:
            continue
        try:
            due_dt = datetime.fromisoformat(due_iso.replace("Z", "+00:00")).astimezone(LOCAL_TZ)
        except ValueError:
            continue

        due_date = due_dt.strftime("%Y-%m-%d")
        due_unix = a.get("due_unix", 0)
        course = COURSE_SHORT.get(a.get("course", ""), a.get("course", ""))

        if due_unix and due_unix < now_unix:
            days_late = max(1, int((now_unix - due_unix) / 86400))
            results.append({"urgency": "overdue", "title": a["title"], "course": course, "days_late": days_late})
        elif due_date == today_str:
            results.append({"urgency": "today", "title": a["title"], "course": course})
        elif due_date == tomorrow_str:
            results.append({"urgency": "tomorrow", "title": a["title"], "course": course})

    return results


def get_dedup_cache():
    if not CACHE_FILE.exists():
        return {}
    try:
        return json.loads(CACHE_FILE.read_text())
    except (json.JSONDecodeError, OSError):
        return {}


def save_dedup_cache(cache):
    CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
    CACHE_FILE.write_text(json.dumps(cache))


def fingerprint(item):
    key = f"{item['urgency']}:{item['title']}"
    return hashlib.md5(key.encode()).hexdigest()[:12]


def build_alerts(assignments, now):
    cache = get_dedup_cache()
    now_unix = now.timestamp()
    hour = now.hour
    in_quiet = hour >= QUIET_START or hour < QUIET_END

    # drop cache entries older than the dedup window
    cache = {k: v for k, v in cache.items() if now_unix - v < DEDUP_WINDOW}

    alerts = []
    for item in assignments:
        # quiet hours: overdue alerts only
        if in_quiet and item["urgency"] != "overdue":
            continue

        fp = fingerprint(item)
        if fp in cache:
            continue

        if item["urgency"] == "overdue":
            msg = f"[OVERDUE] {item['title']} ({item['course']}) - {item['days_late']}d late"
        elif item["urgency"] == "today":
            msg = f"[TODAY] {item['title']} ({item['course']}) - due today"
        else:
            msg = f"[TOMORROW] {item['title']} ({item['course']}) - due tomorrow"

        alerts.append((fp, msg))
        if len(alerts) >= MAX_ALERTS:
            break

    return alerts, cache


def send_telegram(text):
    payload = json.dumps({
        "chat_id": TELEGRAM_CHAT,
        "text": text,
        "disable_web_page_preview": True,
    }).encode()
    req = urllib.request.Request(
        f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
        data=payload,
        headers={"Content-Type": "application/json"},
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            return r.status == 200
    except urllib.error.URLError as e:
        print(f"Telegram error: {e}", file=sys.stderr)
        return False


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--test", action="store_true", help="dry run: print alerts, skip Telegram")
    args = parser.parse_args()

    if not args.test and not (TELEGRAM_TOKEN and TELEGRAM_CHAT):
        sys.exit("Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID env vars (or use --test).")

    now = datetime.now(tz=LOCAL_TZ)
    assignments = load_assignments(now)
    alerts, cache = build_alerts(assignments, now)

    if not alerts:
        print("No alerts to send.", file=sys.stderr)
        return

    for fp, msg in alerts:
        print(msg)
        if not args.test:
            if send_telegram(msg):
                cache[fp] = now.timestamp()
                print("  sent", file=sys.stderr)
            else:
                print("  failed", file=sys.stderr)

    if not args.test:
        save_dedup_cache(cache)


if __name__ == "__main__":
    main()

# src: ezra, contact: ezkru69@mail.com
