#!/usr/bin/env python3
"""
diff-journal.py — periodic git change capture.

For every git repo found under SEARCH_ROOTS, takes a non-destructive
snapshot (`git stash create`, never touches the working tree), diffs it
against the previous run's snapshot, and ships the diffs over ssh to a
receiver script on your server. The receiver reads one JSON object on
stdin: {"day", "diffs": [{"rel", "body"}], "events": [json lines]}.

Setup: edit the config block below, then run every ~30 min from cron or
launchd. Touch PAUSE_FILE to disable capture temporarily.
"""

from __future__ import annotations

import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path

HOME = Path.home()

# ---- config ----
SSH_HOST = "your-server"                          # ssh alias or user@host
RECEIVER = "/path/on/server/change_receiver.py"   # remote script, reads JSON payload from stdin
SEARCH_ROOTS = [HOME / "code", HOME / "projects"]  # where to look for git repos
STATE = HOME / ".local/state/diff-journal/state.json"
PAUSE_FILE = HOME / ".local/state/diff-journal/paused"
DEVICE = "laptop"                                 # label written into events
MAX_DEPTH = 3
MAX_REPOS = 16
MAX_DIFF_BYTES = 200_000
EXCERPT_LINES = 60
# ---- end config ----

EXCLUDES = [
    ":(exclude,glob)**/*.lock",
    ":(exclude,glob)**/*-lock.json",
    ":(exclude,glob)**/package-lock.json",
    ":(exclude,glob)**/Cargo.lock",
    ":(exclude,glob)**/pnpm-lock.yaml",
    ":(exclude,glob)**/*.min.js",
    ":(exclude,glob)**/*.map",
    ":(exclude,glob)**/dist/**",
    ":(exclude,glob)**/build/**",
]


def git(repo: Path, *args: str, ok_fail: bool = False) -> str:
    r = subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True)
    if r.returncode != 0 and not ok_fail:
        raise RuntimeError(r.stderr.strip()[:200])
    return r.stdout


def find_repos() -> list[Path]:
    repos: list[Path] = []
    for root in SEARCH_ROOTS:
        if not root.is_dir():
            continue
        try:
            out = subprocess.run(
                ["find", str(root), "-maxdepth", str(MAX_DEPTH), "-name", ".git"],
                capture_output=True, text=True, timeout=30,
            ).stdout
        except subprocess.TimeoutExpired:
            continue
        for line in out.splitlines():
            p = Path(line)
            if p.name == ".git":
                repos.append(p.parent)
    seen, uniq = set(), []
    for r in repos:
        if r not in seen:
            seen.add(r)
            uniq.append(r)
    return uniq[:MAX_REPOS]


def snapshot_sha(repo: Path) -> str | None:
    try:
        sha = git(repo, "stash", "create").strip()
        if sha:
            return sha
        return git(repo, "rev-parse", "HEAD").strip() or None
    except RuntimeError:
        return None


def slugify(s: str) -> str:
    return "".join(c if c.isalnum() or c == "-" else "-" for c in s.lower()).strip("-")[:60] or "repo"


def main() -> int:
    if PAUSE_FILE.exists():
        return 0

    STATE.parent.mkdir(parents=True, exist_ok=True)
    try:
        state = json.loads(STATE.read_text())
    except (OSError, json.JSONDecodeError):
        state = {}

    now = datetime.now(timezone.utc)
    ts = now.strftime("%Y-%m-%dT%H:%M:%SZ")
    day = now.strftime("%Y-%m-%d")
    stamp = now.strftime("%Y-%m-%dT%H%M%SZ")

    diffs: list[dict] = []
    events: list[str] = []

    for repo in find_repos():
        key = str(repo)
        cur = snapshot_sha(repo)
        if not cur:
            continue
        prev = state.get(key, {}).get("sha")
        state[key] = {"sha": cur, "ts": ts}
        if not prev or prev == cur:
            continue
        try:
            stat = git(repo, "diff", "--stat", prev, cur, "--", ".", *EXCLUDES, ok_fail=True).strip()
            diff = git(repo, "diff", "--unified=1", prev, cur, "--", ".", *EXCLUDES, ok_fail=True)
        except RuntimeError:
            continue
        if not diff.strip():
            continue

        name = repo.name
        slug = slugify(name)
        oversize = len(diff.encode()) > MAX_DIFF_BYTES
        full_rel = f"changes/{slug}/{stamp}.diff"
        body = stat + "\n\n" + (f"[diff omitted: {len(diff.encode())} bytes]" if oversize else diff)
        diffs.append({"rel": full_rel, "body": body})

        files = [ln.split("|")[0].strip() for ln in stat.splitlines() if "|" in ln][:12]
        event = {
            "ts": ts, "source": "code", "kind": "code_change", "path": slug,
            "payload": {
                "repo": name,
                "stat": stat.splitlines()[-1].strip() if stat else "",
                "files": files,
                "diff_excerpt": "\n".join(diff.splitlines()[:EXCERPT_LINES]),
                "full": full_rel,
            },
            "device": DEVICE,
        }
        events.append(json.dumps(event))

    STATE.write_text(json.dumps(state, indent=2))
    if not events:
        print(f"diff-journal: no changes at {ts}")
        return 0

    payload = json.dumps({"day": day, "diffs": diffs, "events": events})
    r = subprocess.run(
        ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", SSH_HOST,
         "python3", RECEIVER],
        input=payload, capture_output=True, text=True,
    )
    if r.returncode != 0:
        print(f"diff-journal: ship FAILED rc={r.returncode}: {r.stderr.strip()[:160]}",
              file=sys.stderr)
        return 1
    print(f"diff-journal: shipped {len(events)} change window(s) -> {r.stdout.strip()}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

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