#!/usr/bin/env python3
"""repo-map.py — Token-budgeted repository structure map.
See SKILL.md for usage. Run: python3 repo-map.py /path/to/repo --budget 1024"""
# Full source maintained at ~/.hermes/scripts/repo-map.py
import subprocess, os, sys, json, argparse
from collections import defaultdict
from datetime import datetime, timezone

LANG_MAP = {
    ".kt": "kotlin", ".kts": "kotlin", ".java": "java",
    ".py": "python", ".js": "js", ".ts": "ts", ".tsx": "tsx",
    ".go": "go", ".rs": "rust", ".rb": "ruby", ".php": "php",
    ".swift": "swift", ".c": "c", ".cpp": "cpp", ".h": "c-header",
    ".hpp": "cpp-header", ".cs": "csharp", ".scala": "scala",
    ".sql": "sql", ".sh": "shell", ".bash": "shell", ".zsh": "shell",
    ".yaml": "yaml", ".yml": "yaml", ".json": "json", ".xml": "xml",
    ".md": "markdown", ".rst": "rst", ".toml": "toml",
    ".gradle": "gradle", ".gradle.kts": "gradle",
    ".ktx": "kotlin", ".proto": "protobuf",
    ".css": "css", ".scss": "scss", ".html": "html",
    ".gradle": "gradle-kts",
}
TYPE_PRIORITY = {"source": 10, "config": 7, "build": 8, "schema": 9,
                 "test": 6, "docs": 4, "data": 3, "other": 2}
IGNORE_DIRS = {".git", "node_modules", "build", ".gradle", "target",
    ".idea", ".vscode", "__pycache__", ".gitlab", ".github", "venv",
    ".venv", ".hermes", "bin", "obj", ".kotlin", ".cxx", "out",
    "dist", ".next", ".turbo", ".cache", "_site", "public", "coverage"}

def get_changed_recently(filepath, days=7):
    try:
        r = subprocess.run(["git", "-C", os.path.dirname(filepath), "log", "-1",
            "--format=%ct", "--", filepath], capture_output=True, text=True, timeout=5)
        return r.returncode == 0 and r.stdout.strip() and \
            (datetime.now(timezone.utc).timestamp() - int(r.stdout.strip())) < days * 86400
    except: return False

def classify_file(fname, ext):
    lang = LANG_MAP.get(ext.lower(), "other")
    bname = os.path.basename(fname).lower()
    if bname in ("build.gradle.kts","build.gradle","makefile","dockerfile"): return "build", lang
    if ext in (".proto",".graphql",".sql"): return "schema", lang
    if "test" in bname or "spec" in bname: return "test", lang
    if lang in ("kotlin","java","python","go","rust","ts","tsx","js","c","cpp"): return "source", lang
    if ext in (".yaml",".yml",".json",".toml",".ini",".conf"): return "config", lang
    if ext in (".md",".rst"): return "docs", lang
    return "other", lang

def build_repo_map(root_path, budget_tokens=1024, max_depth=10):
    root_path = os.path.abspath(root_path)
    if not os.path.isdir(root_path): return []
    files = []
    try:
        result = subprocess.run(["find", root_path, "-type", "f",
            "-not", "-path", "*/.git/*","-not","-path","*/node_modules/*",
            "-not","-path","*/build/*","-not","-path","*/target/*",
            "-not","-path","*/venv/*","-not","-path","*/__pycache__/*",
            "-not","-path","*/.gradle/*","-not","-path","*/dist/*",
            "-not","-path","*/public/*","-not","-path","*/.idea/*",
            "-not","-path","*/site-packages/*",
            "-not","-name","*.lock","-not","-name","*.log",
            "-not","-name","*.class","-not","-name","*.apk",
            "-not","-name","*.png","-not","-name","*.jpg",
            "-not","-name","*.svg","-not","-name","*.ico",
            "-exec","wc","-l","{}","+"],
            capture_output=True, text=True, timeout=15)
        for line in result.stdout.strip().split("\n"):
            if not line.strip(): continue
            parts = line.rsplit(maxsplit=1)
            if len(parts) != 2: continue
            try: line_count = int(parts[0])
            except: continue
            fpath = parts[1]
            rel = os.path.relpath(fpath, root_path)
            if rel.count(os.sep) > max_depth: continue
            if any(p in rel.split(os.sep) for p in IGNORE_DIRS): continue
            ext = os.path.splitext(fpath)[1].lower()
            ftype, lang = classify_file(fpath, ext)
            changed = get_changed_recently(fpath)
            score = TYPE_PRIORITY.get(ftype, 2) * 10 + (15 if changed else 0)
            if line_count < 500: score += 5
            if line_count < 50: score -= 3
            bname = os.path.basename(fpath).lower()
            if bname in ("license","licence","readme.md",".gitignore"): score = 1
            files.append({"path": rel, "lines": line_count, "lang": lang,
                          "type": ftype, "score": score, "changed": changed})
    except subprocess.TimeoutExpired: return None
    except FileNotFoundError: return None
    files.sort(key=lambda f: f["score"], reverse=True)
    selected, used = [], 0
    for f in files:
        if used + 12 > budget_tokens: break
        selected.append(f); used += 12
    return selected

def format_as_tree(files, root):
    if not files: return "(empty)"
    lines = [f"# Repo Map — {os.path.basename(root)}",
             f"**{len(files)} files, ~{sum(f['lines'] for f in files)} lines**",
             f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n",
             "| Type | Lang | File | Lines |",
             "|------|------|------|-------|"]
    for f in files:
        name = f["path"]
        if len(name) > 60: name = "..." + name[-57:]
        lines.append(f"| {f['type']:6s} | {f['lang']:8s} | {name}{' 🔄' if f['changed'] else ''} | {f['lines']:>5d} |")
    return "\n".join(lines) + f"\n\n*🔄 = modified in last 7 days*"

def main():
    p = argparse.ArgumentParser()
    p.add_argument("path", nargs="?", default=".")
    p.add_argument("--budget", type=int, default=1024)
    p.add_argument("--depth", type=int, default=10)
    p.add_argument("--json", action="store_true")
    args = p.parse_args()
    files = build_repo_map(args.path, args.budget, args.depth)
    if files is None: sys.exit(1)
    print(json.dumps(files, indent=2) if args.json else format_as_tree(files, args.path))

if __name__ == "__main__": main()
