#!/usr/bin/python3
# cal-notif: calendar notifications with local spoken announcements.
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
"""Usage: cal-notif daemon | pick | say TEXT

daemon  watch the calendars and notify (popup + voice) at each alarm
pick    rofi picker to set custom alarm offsets for one event
say     speak TEXT with the configured voice
"""

import json
import os
import re
import shutil
import subprocess
import sys
import time
import tomllib
from collections import namedtuple
from datetime import datetime, time as dtime, timedelta, timezone
from pathlib import Path

from dateutil.rrule import rrulestr
from icalendar import Calendar, vRecur

CONF_DIR = Path(os.environ.get("XDG_CONFIG_HOME", "~/.config")).expanduser() / "cal-notif"
DEFAULTS = {
    "calendars_dir": "~/.local/share/calendars",
    "snooze": "5m",
    "late": "10m",
    "rofi_theme": "~/.config/rofi/udt/list.rasi",
    "defaults": {"*": []},
    "voice": {"enabled": True,
              "model": "/data/voice-models/kokoro/kokoro-v1.0.onnx",
              "voices": "/data/voice-models/kokoro/voices-v1.0.bin",
              "lang": "it",
              "blend": {"if_sara": 0.8, "af_bella": 0.2},
              "lead_silence": 0.3,
              "say": "Tra {in}: {summary}",
              "say_now": "Adesso: {summary}"},
}
PICK_DAYS = 30

# ---- durations ----

DUR = re.compile(r"(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?")


def parse_duration(s):
    m = DUR.fullmatch(s.replace(" ", ""))
    if not s.strip() or not m:
        raise ValueError(f"bad duration {s!r}, want e.g. '1d 2h 15m'")
    d, h, mi = (int(x or 0) for x in m.groups())
    return timedelta(days=d, hours=h, minutes=mi)


def fmt_duration(td):
    m = round(td.total_seconds() / 60)
    d, m = divmod(m, 1440)
    h, m = divmod(m, 60)
    return " ".join(f"{n}{u}" for n, u in ((d, "d"), (h, "h"), (m, "m")) if n) or "0m"


UNITS = ["", "uno", "due", "tre", "quattro", "cinque", "sei", "sette", "otto", "nove",
         "dieci", "undici", "dodici", "tredici", "quattordici", "quindici", "sedici",
         "diciassette", "diciotto", "diciannove"]
TENS = ["", "", "venti", "trenta", "quaranta", "cinquanta", "sessanta", "settanta",
        "ottanta", "novanta"]


def number_it(n):
    # 1..99; ventuno, ventotto: the tens drop their vowel before uno/otto.
    if n < 20:
        return UNITS[n]
    t, u = divmod(n, 10)
    return (TENS[t][:-1] if u in (1, 8) else TENS[t]) + UNITS[u]


def spoken_it(td):
    m = round(td.total_seconds() / 60)
    d, m = divmod(m, 1440)
    h, m = divmod(m, 60)
    parts = []
    for n, one, many in ((d, "un giorno", "giorni"), (h, "un'ora", "ore"), (m, "un minuto", "minuti")):
        if n == 1:
            parts.append(one)
        elif n:
            # ponytail: number_it covers 1-99; bigger counts are read as digits.
            parts.append(f"{number_it(n) if n < 100 else n} {many}")
    if not parts:
        return ""
    return parts[0] if len(parts) == 1 else ", ".join(parts[:-1]) + " e " + parts[-1]

# ---- config ----

def load_config(path=None):
    path = path or CONF_DIR / "config.toml"
    cfg = {k: dict(v) if isinstance(v, dict) else v for k, v in DEFAULTS.items()}
    if path.exists():
        with open(path, "rb") as f:
            user = tomllib.load(f)
        for k, v in user.items():
            cfg[k] = {**cfg[k], **v} if k == "voice" else v
    for k in ("snooze", "late"):             # fail early on bad durations
        parse_duration(cfg[k])
    for v in cfg["defaults"].values():
        for o in (v.get("offsets", []) if isinstance(v, dict) else v):
            parse_duration(o)
    return cfg


def load_overrides(path=None):
    path = path or CONF_DIR / "overrides.toml"
    if not path.exists():
        return {}
    with open(path, "rb") as f:
        return tomllib.load(f)


def save_overrides(ov, path=None):
    path = path or CONF_DIR / "overrides.toml"
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(".tmp")
    # JSON strings and string arrays are valid TOML, so no TOML writer is needed.
    tmp.write_text("".join(f"{json.dumps(uid)} = {json.dumps(offs)}\n"
                           for uid, offs in sorted(ov.items())))
    os.replace(tmp, path)

# ---- calendars ----

# ---- alarms ----

# ---- voice ----

# ---- daemon ----

# ---- picker ----


def main():
    sys.exit(__doc__)


if __name__ == "__main__":
    main()
