#!/usr/bin/env python3
"""Vault stats — growth metrics for an Obsidian vault.

Reports how the vault is growing: total note count, word/character volume, size
on disk, a per-area breakdown, and — by diffing against a persisted daily history
plus git — how much was *added* recently (new notes this week / since last run,
and which areas are growing fastest).

Stdlib-only (`python3`), no venv. Finds the vault root the same way `digest.py`
does (walk up to the folder holding `Areas/` + `AGENTS.md`, or `$VAULT_DIR`), so
it just works from anywhere in the repo. Read-only against the vault; the only
thing it writes is its own append-only `history.json` next to this file (one
snapshot per calendar day, latest run wins) so run-over-run trends survive.

Usage:
    python3 Agent/scripts/vault-stats/vault_stats.py                # human summary + save today's snapshot
    python3 Agent/scripts/vault-stats/vault_stats.py --json         # machine-readable dump
    python3 Agent/scripts/vault-stats/vault_stats.py --since 30d    # window for the git "new notes" line
    python3 Agent/scripts/vault-stats/vault_stats.py --top 12       # how many areas to list
    python3 Agent/scripts/vault-stats/vault_stats.py --no-save      # compute without touching history.json
"""
import os
import sys
import json
import argparse
import subprocess
import datetime as dt
from pathlib import Path
from collections import defaultdict

from alexandria_vault.engine import consume_vault_arg, resolve_vault

# `--vault` must land in VAULT_DIR before the module-level root/history resolution.
consume_vault_arg()

# Directories that aren't vault *content* — tooling, caches, VCS internals.
EXCLUDE_DIRS = {".git", ".obsidian", ".trash", ".claude", ".state", ".alexandria", "node_modules", ".venv"}


# The run-over-run history stays IN the vault (its growth data is per-vault, and this
# is where it has always lived), not beside this file — which is app-side now.
HISTORY_PATH = resolve_vault() / "Agent" / "scripts" / "vault-stats" / "history.json"


def today_local() -> dt.date:
    """Match the digest's notion of 'today' (the owner's resolved local date via
    DIGEST_TODAY) so a stats run wired into the sweep stamps the right calendar day."""
    iso = os.environ.get("DIGEST_TODAY")
    if iso:
        try:
            return dt.date.fromisoformat(iso)
        except ValueError:
            pass
    return dt.date.today()


def iter_files(root: Path):
    for dirpath, dirnames, filenames in os.walk(root):
        # prune excluded + hidden dirs in place so os.walk doesn't descend
        dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS and not d.startswith(".")]
        for name in filenames:
            if name.startswith("."):
                continue
            yield Path(dirpath) / name


def area_of(rel: Path) -> str:
    """Bucket a note by its home: top-level folder, or `Areas/<Area>` one level deep."""
    parts = rel.parts
    if len(parts) <= 1:
        return "(root)"
    if parts[0] == "Areas" and len(parts) >= 2:
        return f"Areas/{parts[1]}"
    return parts[0]


def collect(root: Path) -> dict:
    notes = words = chars = note_bytes = files_all = repo_bytes = 0
    by_notes = defaultdict(int)
    by_words = defaultdict(int)
    by_bytes = defaultdict(int)
    for path in iter_files(root):
        try:
            size = path.stat().st_size
        except OSError:
            continue
        files_all += 1
        repo_bytes += size
        if path.suffix.lower() != ".md":
            continue
        try:
            text = path.read_text(encoding="utf-8", errors="ignore")
        except OSError:
            continue
        w = len(text.split())
        area = area_of(path.relative_to(root))
        notes += 1
        words += w
        chars += len(text)
        note_bytes += size
        by_notes[area] += 1
        by_words[area] += w
        by_bytes[area] += size
    return {
        "notes": notes,
        "words": words,
        "chars": chars,
        "note_bytes": note_bytes,
        "files_all": files_all,
        "repo_bytes": repo_bytes,
        "by_area_notes": dict(by_notes),
        "by_area_words": dict(by_words),
        "by_area_bytes": dict(by_bytes),
    }


def parse_since(since: str) -> str:
    """Accept `7d`, `2w`, an ISO date, or any git-native '--since' phrase."""
    s = since.strip()
    if s and s[-1] in "dwmy" and s[:-1].isdigit():
        unit = {"d": "days", "w": "weeks", "m": "months", "y": "years"}[s[-1]]
        return f"{s[:-1]} {unit} ago"
    return s


def git_new_notes(root: Path, since: str):
    """Markdown files added (A) in the window, per git. None if git is unavailable."""
    try:
        out = subprocess.run(
            ["git", "-C", str(root), "log", f"--since={parse_since(since)}",
             "--diff-filter=A", "--name-only", "--pretty=format:"],
            capture_output=True, text=True, timeout=30,
        )
    except Exception:
        return None
    if out.returncode != 0:
        return None
    files = {ln.strip() for ln in out.stdout.splitlines()
             if ln.strip().endswith(".md")}
    # keep only paths still present + inside content dirs
    kept = []
    for f in sorted(files):
        p = root / f
        if p.exists() and not any(part in EXCLUDE_DIRS for part in Path(f).parts):
            kept.append(f)
    return kept


def load_history() -> list:
    if HISTORY_PATH.exists():
        try:
            return json.loads(HISTORY_PATH.read_text(encoding="utf-8"))
        except Exception:
            return []
    return []


def save_history(history: list, snap: dict) -> None:
    kept = [h for h in history if h.get("date") != snap["date"]]
    kept.append(snap)
    kept.sort(key=lambda h: h.get("date", ""))
    HISTORY_PATH.parent.mkdir(parents=True, exist_ok=True)
    HISTORY_PATH.write_text(json.dumps(kept, indent=1) + "\n", encoding="utf-8")


def human_bytes(n: int) -> str:
    x = float(n)
    for unit in ("B", "KB", "MB", "GB"):
        if x < 1024 or unit == "GB":
            return f"{x:.0f} {unit}" if unit == "B" else f"{x:.1f} {unit}"
        x /= 1024
    return f"{x:.1f} GB"


def render(stats, today, prior, new_notes, since, top):
    L = []
    L.append(f"# Vault stats — {today.isoformat()}")
    L.append("")
    L.append(f"**{stats['notes']:,} notes** · {stats['words']:,} words · "
             f"{human_bytes(stats['note_bytes'])} of notes · "
             f"{stats['files_all']:,} files · {human_bytes(stats['repo_bytes'])} on disk")
    L.append("")

    # Growth vs the last saved snapshot
    if prior:
        dn = stats["notes"] - prior.get("notes", 0)
        dw = stats["words"] - prior.get("words", 0)
        L.append(f"**Since {prior['date']}:** {dn:+,} notes · {dw:+,} words")
    else:
        L.append("_First snapshot — run again later for run-over-run deltas._")

    # Git window (accurate "new notes added" over a real time window)
    if new_notes is not None:
        L.append(f"**New notes ({since}, per git):** {len(new_notes):+,}")
    L.append("")

    # Top areas by size
    ranked = sorted(stats["by_area_notes"].items(), key=lambda kv: kv[1], reverse=True)
    L.append(f"## Biggest areas (of {len(ranked)})")
    for area, n in ranked[:top]:
        w = stats["by_area_words"].get(area, 0)
        L.append(f"- **{area}** — {n:,} notes · {w:,} words")

    # Fastest-growing since prior snapshot (needs history)
    if prior and prior.get("by_area_notes"):
        deltas = []
        pa = prior["by_area_notes"]
        for area, n in stats["by_area_notes"].items():
            d = n - pa.get(area, 0)
            if d:
                deltas.append((area, d))
        deltas.sort(key=lambda kv: kv[1], reverse=True)
        gainers = [d for d in deltas if d[1] > 0][:top]
        if gainers:
            L.append("")
            L.append(f"## Fastest-growing since {prior['date']}")
            for area, d in gainers:
                L.append(f"- **{area}** — {d:+,} notes")

    return "\n".join(L)


def main():
    ap = argparse.ArgumentParser(description="Vault growth stats.")
    ap.add_argument("--json", action="store_true", help="machine-readable dump")
    ap.add_argument("--since", default="7d", help="window for the git new-notes line (e.g. 7d, 2w, 2026-06-01)")
    ap.add_argument("--top", type=int, default=10, help="how many areas to list")
    ap.add_argument("--no-save", action="store_true", help="don't write today's snapshot to history.json")
    ap.add_argument("--history", action="store_true", help="print the saved history and exit")
    args = ap.parse_args()

    root = resolve_vault()
    history = load_history()

    if args.history:
        print(json.dumps(history, indent=1))
        return

    today = today_local()
    stats = collect(root)
    new_notes = git_new_notes(root, args.since)

    # most recent snapshot from a prior day (so today's re-runs compare to yesterday, not to themselves)
    prior = None
    for h in reversed(history):
        if h.get("date", "") < today.isoformat():
            prior = h
            break

    snap = {
        "date": today.isoformat(),
        "notes": stats["notes"],
        "words": stats["words"],
        "chars": stats["chars"],
        "note_bytes": stats["note_bytes"],
        "files_all": stats["files_all"],
        "repo_bytes": stats["repo_bytes"],
        "by_area_notes": stats["by_area_notes"],
    }

    if args.json:
        out = dict(snap)
        out["by_area_words"] = stats["by_area_words"]
        out["by_area_bytes"] = stats["by_area_bytes"]
        out["prior"] = prior
        out["new_notes_since"] = args.since
        out["new_notes"] = new_notes
        out["vault_root"] = str(root)
        print(json.dumps(out, indent=1))
    else:
        print(render(stats, today, prior, new_notes, args.since, args.top))

    if not args.no_save:
        save_history(history, snap)


if __name__ == "__main__":
    main()
