#!/bin/bash
# ============================================================
# caldav-sync.sh — CalDAV calendar sync stack
# ============================================================
# Sets up a full offline-capable CalDAV synchronisation stack:
#
#   vdirsyncer     — syncs CalDAV server ↔ local ICS files
#   khal           — CLI/TUI calendar viewer and editor
#   python-icalendar — Python library for parsing .ics files
#   ics-to-calendarim — custom script that converts ICS files
#                       into the JSON cache format expected by
#                       the calendar.vim Neovim plugin
#
# A systemd user timer fires every 15 minutes to keep local
# calendars current without any manual intervention.
#
# This is an optional module because CalDAV sync is only useful
# if the user has a remote calendar server (Nextcloud, Radicale,
# Google Calendar export, etc.).
# ============================================================

set -euo pipefail
# Load shared logging helpers from the dotfiles lib
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"

# ── Core packages ─────────────────────────────────────────────────────────────
# vdirsyncer       : the CalDAV/CardDAV sync daemon that mirrors remote
#                    calendars to local Maildir-style .ics files
# khal             : a CLI/TUI calendar reader/editor that reads the local
#                    ICS files and presents them in a human-friendly view
# python-icalendar : Python library needed by the ics-to-calendarim helper
#                    to parse VEVENT components from .ics files
log "Installing CalDAV sync stack..."
sudo pacman -S --noconfirm --needed vdirsyncer khal python-icalendar

# In unattended installs (answerfile mode / no TTY) there is no operator to answer
# the server prompts below, so install the tools and stop cleanly here; the user
# configures their CalDAV account after first boot.
if [[ "${MARCHY_UNATTENDED:-0}" == "1" || ! -t 0 ]]; then
    skip "Unattended mode — CalDAV tools installed; configure the account after first boot."
    exit 0
fi

# ── Credentials ───────────────────────────────────────────────────────────────
# Collect the minimum information needed to configure vdirsyncer.
# -r = raw input (no backslash escaping), -p = prompt, -s = silent (password)
read -rp  "CalDAV server URL (e.g. https://dav.example.com/cal/): " CALDAV_URL
read -rp  "Username                                              : " CALDAV_USER
read -rsp "Password                                              : " CALDAV_PASS; echo
read -rp  "Calendar display name [Personal]                      : " CAL_NAME
# Default to "Personal" if the user pressed Enter without typing a name
CAL_NAME="${CAL_NAME:-Personal}"

# Local directory where vdirsyncer will store the .ics files, one sub-
# directory per calendar collection discovered on the server.
CALDAV_DIR="$HOME/.local/share/calendars"
mkdir -p "$CALDAV_DIR"

# ── vdirsyncer configuration ──────────────────────────────────────────────────
# vdirsyncer reads a single INI-style config file.
# We create the status directory first (vdirsyncer stores sync state there
# to track what has been pushed/pulled for each item).
log "Writing ~/.config/vdirsyncer/config..."
mkdir -p ~/.config/vdirsyncer ~/.local/share/vdirsyncer/status

# The config defines:
#   [general]         — global options (where to store sync state)
#   [pair calendars]  — a named pair that links local ↔ remote storage
#   [storage *]       — individual storage backends (filesystem + caldav)
# "collections = ["from b"]" means: discover all collections that exist on
# the remote (b) side and create matching local directories automatically.
cat > ~/.config/vdirsyncer/config << EOF
[general]
status_path = "$HOME/.local/share/vdirsyncer/status/"

[pair calendars]
a = "local_cals"
b = "remote_cals"
collections = ["from b"]
metadata = ["color", "displayname"]

[storage local_cals]
type = "filesystem"
path = "$CALDAV_DIR"
fileext = ".ics"

[storage remote_cals]
type = "caldav"
url = "$CALDAV_URL"
username = "$CALDAV_USER"
password = "$CALDAV_PASS"
EOF
# Restrict permissions: config contains plaintext password
chmod 600 ~/.config/vdirsyncer/config

# "discover" queries the server for all available collections and records them
# in the status directory so future `vdirsyncer sync` commands know what to sync.
# `yes |` pre-answers any interactive "create local directory?" prompts.
# `|| true` prevents the script from aborting if discovery finds nothing new.
log "Discovering CalDAV collections (confirm any prompts with y)..."
yes | vdirsyncer discover calendars || true

# Pull all events from the server for the first time.
# Best-effort: with placeholder credentials or no network at install time this
# is expected to fail, which must not abort the module. Warn and continue.
log "Running initial sync..."
vdirsyncer sync || warn "Initial CalDAV sync failed (credentials/network not ready) — run 'vdirsyncer sync' once configured."

# ── khal configuration ────────────────────────────────────────────────────────
# khal needs one [[calendar]] section per local ICS directory.
# We use an inline Python script so we can iterate over discovered directories
# dynamically instead of hard-coding calendar names.
log "Writing ~/.config/khal/config (per-calendar entries)..."
mkdir -p ~/.config/khal
python3 - "$CALDAV_DIR" << 'PYEOF'
import sys
from pathlib import Path

cal_root = Path(sys.argv[1])
# Cycle through a set of terminal colours so different calendars are visually
# distinct in khal's output.
colors   = ["light blue", "light green", "light red", "light magenta", "light cyan"]
dirs     = sorted(d for d in cal_root.iterdir() if d.is_dir())

lines = ["[calendars]"]
for i, d in enumerate(dirs):
    # Each sub-directory created by vdirsyncer is one calendar collection.
    lines += [f"  [[{d.name}]]", f"    path = {d}", f"    color = {colors[i % len(colors)]}"]

# [sqlite] — khal caches parsed events here for faster startup
# [locale] — display preferences for times and dates
lines += [
    "", "[sqlite]", "path = ~/.local/share/khal/khal.db",
    "", "[locale]", "timeformat = %H:%M", "dateformat = %Y-%m-%d",
    "datetimeformat = %Y-%m-%d %H:%M",
]

(Path.home() / ".config/khal/config").write_text("\n".join(lines) + "\n")
PYEOF

# ── ICS → calendar.vim cache converter ───────────────────────────────────────
# calendar.vim (a Neovim plugin) expects event data in a specific JSON cache
# format at ~/.cache/calendar.vim/local/.  This helper script reads the local
# ICS files maintained by vdirsyncer and writes that JSON cache so calendar.vim
# can show events without hitting the network.
log "Installing ics-to-calendarim converter..."
mkdir -p ~/.local/bin
cat > ~/.local/bin/ics-to-calendarim << 'PYEOF'
#!/usr/bin/env python3
"""
Convert vdirsyncer ICS files to calendar.vim local event cache.
Cache lives at ~/.cache/calendar.vim/local/ in the format:
  calendarList          – list of calendars (JSON)
  <id>/<YYYY>/<MM>/0   – events per month (JSON)
"""
import json, sys
from pathlib import Path
from datetime import datetime, date, timezone

try:
    from icalendar import Calendar as iCal
except ImportError:
    sys.exit("python-icalendar is required: sudo pacman -S python-icalendar")

CALDAV_DIR = Path.home() / ".local/share/calendars"
CACHE_DIR  = Path.home() / ".cache/calendar.vim/local"
KHAL_CFG   = Path.home() / ".config/khal/config"

# Hex colours for calendar.vim's event display (one per calendar, cycled)
COLORS      = ["#4CAF50", "#2196F3", "#FF5722", "#9C27B0", "#FF9800"]
# Matching terminal colour names for khal (kept in sync so both tools agree)
KHAL_COLORS = ["light blue", "light green", "light red", "light magenta", "light cyan"]

def to_naive(dt):
    """Normalise a DTSTART/DTEND value to a naive string + metadata.

    Returns (string_repr, is_timed, year, month).
    Timezone-aware datetimes are converted to UTC before stripping tzinfo
    so that comparisons are consistent across DST transitions.
    All-day dates return an ISO date string (YYYY-MM-DD) with is_timed=False.
    """
    if isinstance(dt, datetime):
        if dt.tzinfo is not None:
            dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
        return dt.strftime("%Y-%m-%dT%H:%M:%S"), True, dt.year, dt.month
    if isinstance(dt, date):
        return dt.strftime("%Y-%m-%d"), False, dt.year, dt.month
    return None, False, None, None

def main():
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    cal_items = []  # list of calendar metadata objects for calendarList

    # Iterate over each calendar collection directory (one per CalDAV calendar)
    for idx, cal_dir in enumerate(sorted(d for d in CALDAV_DIR.iterdir() if d.is_dir())):
        cal_id      = cal_dir.name
        # Human-readable name: replace underscores with spaces and title-case
        cal_summary = cal_dir.name.replace("_", " ").title()
        cal_items.append({
            "id": cal_id,
            "summary": cal_summary,
            "backgroundColor": COLORS[idx % len(COLORS)],
            "foregroundColor": "#ffffff",
        })

        by_month: dict = {}  # keyed by (year, month) → list of event objects
        for ics in cal_dir.glob("*.ics"):
            try:
                cal = iCal.from_ical(ics.read_bytes())
            except Exception as e:
                print(f"warning: skipping {ics.name}: {e}", file=sys.stderr)
                continue
            # Walk all VEVENT components in the ICS file
            for comp in cal.walk("VEVENT"):
                dtstart = comp.get("DTSTART")
                # If DTEND is missing, fall back to DTSTART (zero-duration event)
                dtend   = comp.get("DTEND") or comp.get("DTSTART")
                if not dtstart:
                    continue
                s_str, is_timed, yr, mo = to_naive(dtstart.dt)
                e_str, _,        _,  _  = to_naive(dtend.dt)
                if not s_str:
                    continue
                uid     = str(comp.get("UID", ics.stem))
                summary = str(comp.get("SUMMARY", ""))
                key = (yr, mo)
                # Group events by month so we write one JSON file per month
                by_month.setdefault(key, []).append({
                    "id":      uid,
                    "summary": summary,
                    # calendar.vim distinguishes timed events ("dateTime") from
                    # all-day events ("date") by which key is present
                    "start":   {"dateTime": s_str} if is_timed else {"date": s_str},
                    "end":     {"dateTime": e_str} if is_timed else {"date": e_str},
                })

        # Write one JSON file per (calendar, year, month) combination.
        # Path structure mirrors what calendar.vim expects for its local cache.
        for (yr, mo), events in by_month.items():
            out = CACHE_DIR / cal_id / f"{yr:04d}" / f"{mo:02d}"
            out.mkdir(parents=True, exist_ok=True)
            (out / "0").write_text(json.dumps({"items": events}))

    # Write the top-level calendar list so calendar.vim knows which calendars exist
    (CACHE_DIR / "calendarList").write_text(json.dumps({"items": cal_items}))

    # Also regenerate the khal config so any newly discovered calendars are
    # automatically added without requiring a manual config edit.
    cal_dirs = sorted(d for d in CALDAV_DIR.iterdir() if d.is_dir())
    khal_lines = ["[calendars]"]
    for i, d in enumerate(cal_dirs):
        khal_lines += [f"  [[{d.name}]]", f"    path = {d}",
                       f"    color = {KHAL_COLORS[i % len(KHAL_COLORS)]}"]
    khal_lines += ["", "[sqlite]", "path = ~/.local/share/khal/khal.db",
                   "", "[locale]", "timeformat = %H:%M", "dateformat = %Y-%m-%d",
                   "datetimeformat = %Y-%m-%d %H:%M"]
    KHAL_CFG.write_text("\n".join(khal_lines) + "\n")

    print(f"calendar.vim cache updated: {len(cal_items)} calendar(s).")

if __name__ == "__main__":
    main()
PYEOF
# Make the helper executable so it can be called directly from the PATH
chmod +x ~/.local/bin/ics-to-calendarim

# Build the initial calendar.vim cache from the events we just synced.
# Best-effort: harmless to skip if the first sync pulled nothing.
log "Running initial calendar.vim cache build..."
~/.local/bin/ics-to-calendarim || warn "calendar.vim cache build skipped — rebuild later with: ics-to-calendarim"

# ── systemd user timer ────────────────────────────────────────────────────────
# A systemd user timer keeps calendars current automatically.
# Using systemd (instead of cron) lets the unit run without requiring root and
# integrates cleanly with the user's login session.
#
# Best-effort: when the installer runs from a bare TTY or chroot there is no
# per-user systemd session bus (and ~/.config may not yet be owned by the
# invoking user), so `systemctl --user` and even the mkdir can fail. That must
# not abort the module — the timer can be (re)created on first login. We warn
# and continue rather than letting `set -e` kill the install.
log "Installing vdirsyncer systemd user timer (every 15 min)..."
if mkdir -p ~/.config/systemd/user 2>/dev/null; then
    # The service unit runs vdirsyncer sync then rebuilds the calendar.vim cache.
    # Type=oneshot: the service is considered active only while the process runs;
    # it exits when done rather than staying in the background.
    # After=network-online.target: ensures we don't try to sync before the network
    # is available (important at boot).
    cat > ~/.config/systemd/user/vdirsyncer.service << EOF
[Unit]
Description=Sync CalDAV and rebuild calendar.vim cache
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/vdirsyncer sync
ExecStartPost=$HOME/.local/bin/ics-to-calendarim
EOF

    # The timer unit triggers the service on a schedule.
    # OnBootSec=2min  : first run 2 minutes after login (let the network settle)
    # OnUnitActiveSec=15min : repeat every 15 minutes after the last activation
    cat > ~/.config/systemd/user/vdirsyncer.timer << EOF
[Unit]
Description=Run vdirsyncer every 15 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
Unit=vdirsyncer.service

[Install]
WantedBy=timers.target
EOF

    # A successful daemon-reload means a user session bus is available; only then
    # is it worth trying to enable/start the timer.
    if systemctl --user daemon-reload 2>/dev/null; then
        # Enable and immediately start the timer; "--now" starts it without a reboot.
        systemctl --user enable --now vdirsyncer.timer 2>/dev/null \
            || warn "Could not enable vdirsyncer.timer now — run: systemctl --user enable --now vdirsyncer.timer"
    else
        warn "No user systemd session detected — enable the timer after login with: systemctl --user enable --now vdirsyncer.timer"
    fi
else
    warn "Could not create ~/.config/systemd/user — skipping the vdirsyncer timer (set it up after first login)."
fi

log "CalDAV sync configured. Events will appear in calendar.vim automatically."
log "Manual sync:   vdirsyncer sync && ics-to-calendarim"
log "CLI calendar:  khal list  /  khal interactive"
log "Timer status:  systemctl --user status vdirsyncer.timer"
