#!/usr/bin/env python3
"""
vault-curator — daily mechanical hygiene sweep over HQ-Vault.

Detects items that need Rocky's attention and appends them to
HQ-Vault/_System/CuratorQueue.md (deduplicated against existing entries).
Appends a 1-line summary to HQ-Vault/_System/CuratorLog.md.

Detects:
  1. Files modified in last 7 days missing `updated:` frontmatter field
  2. Folders that are empty (only .gitkeep) and >14 days old

Run from repo root:
    python3 scripts/vault-curator.py

Designed to be called by cron daily at 22:05 UTC (after vault-health.sh
runs at 21:05). auto-sync.sh picks up CuratorQueue/CuratorLog changes
on its next 30-min cycle.
"""
import re
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path

ROOT = Path("HQ-Vault")
QUEUE = ROOT / "_System" / "CuratorQueue.md"
LOG = ROOT / "_System" / "CuratorLog.md"
NOW = datetime.now(timezone.utc)
TODAY = NOW.strftime("%Y-%m-%d")

# Folders that are intentionally always-empty placeholders — don't flag.
EMPTY_FOLDER_ALLOWLIST = {
    "HQ-Vault/_System/Scratch",          # gitignored agent scratch
    "HQ-Vault/_System/Bootstraps",       # awaiting bootstrap content
    "HQ-Vault/_System/HQ-Findings",      # awaiting investigation outputs
    "HQ-Vault/_System/Reviews",          # awaiting periodic snapshot reviews
}


def get_recent_md_files(days: int) -> list[Path]:
    """Files modified within `days` days, via git log (works in repo)."""
    cutoff = (NOW - timedelta(days=days)).strftime("%Y-%m-%d")
    try:
        out = subprocess.check_output(
            ["git", "log", f"--since={cutoff}", "--name-only", "--pretty=format:"],
            text=True,
            encoding="utf-8",
        )
    except subprocess.CalledProcessError:
        return []
    files = {line for line in out.splitlines() if line.startswith("HQ-Vault/") and line.endswith(".md")}
    return sorted({Path(f) for f in files if Path(f).exists()})


def has_updated_field(path: Path) -> bool:
    """Return True if file has `updated:` field in YAML frontmatter."""
    try:
        text = path.read_text(encoding="utf-8", errors="ignore")
    except OSError:
        return True  # don't flag unreadable files
    if not text.startswith("---\n"):
        return True  # no frontmatter at all — different problem, separate item
    end = text.find("\n---", 4)
    if end < 0:
        return True
    fm = text[4:end]
    return any(line.strip().startswith("updated:") for line in fm.splitlines())


def has_any_frontmatter(path: Path) -> bool:
    try:
        return path.read_text(encoding="utf-8", errors="ignore").startswith("---\n")
    except OSError:
        return True


def find_orphan_folders() -> list[Path]:
    """Folders containing only .gitkeep (or nothing) and older than 14 days."""
    cutoff = NOW - timedelta(days=14)
    orphans = []
    for d in ROOT.rglob("*"):
        if not d.is_dir() or d.name.startswith(".") or "_Archive" in d.parts:
            continue
        if d.as_posix() in EMPTY_FOLDER_ALLOWLIST:
            continue
        contents = [p for p in d.iterdir() if p.name != ".gitkeep"]
        if contents:
            continue
        try:
            mtime = datetime.fromtimestamp(d.stat().st_mtime, timezone.utc)
        except OSError:
            continue
        if mtime < cutoff:
            orphans.append(d)
    return sorted(orphans)


def existing_queue_keys() -> set[str]:
    """Set of file/folder paths already mentioned in CuratorQueue.md to dedupe."""
    if not QUEUE.exists():
        return set()
    text = QUEUE.read_text(encoding="utf-8", errors="ignore")
    paths = set(re.findall(r"`([^`]+\.md)`", text))
    paths.update(re.findall(r"`([^`]+/)`", text))
    return paths


def main() -> int:
    new_items: list[str] = []
    seen = existing_queue_keys()

    # Check 1: files modified in last 7 days missing `updated:` frontmatter
    fm_offenders = []
    for f in get_recent_md_files(7):
        rel = f.as_posix()
        if rel.startswith("HQ-Vault/01_Daily/"):
            continue  # dailies skip frontmatter by convention
        if rel.startswith("HQ-Vault/_Archive/"):
            continue
        if not has_any_frontmatter(f):
            continue  # separate concern (no FM at all); not this sweep's scope
        if has_updated_field(f):
            continue
        if rel in seen:
            continue
        fm_offenders.append(rel)

    for rel in fm_offenders[:20]:  # cap at 20 per run to avoid queue explosion
        new_items.append(
            f"- {TODAY} — `{rel}` modified within last 7d but frontmatter "
            f"missing `updated:` field. Add or confirm convention. → vault-curator daily"
        )

    # Check 2: orphan folders >14 days
    orphan_offenders = []
    for d in find_orphan_folders():
        rel = d.as_posix() + "/"
        if rel in seen:
            continue
        orphan_offenders.append(rel)

    for rel in orphan_offenders:
        new_items.append(
            f"- {TODAY} — `{rel}` is empty (only .gitkeep) and unchanged for "
            f">14 days. Populate or remove. → vault-curator daily"
        )

    # Append findings to CuratorQueue.md
    if new_items:
        with QUEUE.open("a", encoding="utf-8") as f:
            f.write(f"\n--- vault-curator daily {TODAY} ---\n\n")
            for item in new_items:
                f.write(item + "\n")

    # Always append a one-line summary to CuratorLog.md
    summary = (
        f"## {TODAY} — vault-curator daily\n\n"
        f"- Frontmatter sweep: {len(fm_offenders)} files missing `updated:` "
        f"(queued: {min(len(fm_offenders), 20)})\n"
        f"- Orphan folders: {len(orphan_offenders)}\n"
        f"- Total queued: {len(new_items)}\n"
    )
    with LOG.open("a", encoding="utf-8") as f:
        f.write("\n" + summary)

    print(f"vault-curator: {len(new_items)} items queued for {TODAY}")

    # Emit structured event to AgentLog
    import subprocess
    try:
        subprocess.run(
            ['bash', 'scripts/log-event.sh', 'cron:vault-curator', 'cron_run',
             'vault-curator.py', 'ok',
             f'queued {len(new_items)} items ({len(fm_offenders)} fm, {len(orphan_offenders)} orphan)'],
            check=False, timeout=5,
        )
    except Exception:
        pass
    return 0


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