#!/usr/bin/env python3
"""
Performance Guardrails — Lightweight Pre-Commit + On-Demand Checks

Two modes:
  1. STATIC (pre-commit): Check for known perf anti-patterns in staged files
  2. BASELINE (on-demand): Measure and compare bundle size (Next.js, Vite, etc.)

Usage:
  perf_guardrails.py              # Static check on staged files
  perf_guardrails.py --bundle     # Check build output size
  perf_guardrails.py --baseline   # Save current baseline
  perf_guardrails.py --compare    # Compare against baseline

Static patterns detected:
  - N+1 queries: ORM access in loops without eager loading
  - Unbounded queries: Missing LIMIT on list endpoints
  - Heavy imports: Importing heavy libraries in API handlers
  - Fetch in useEffect: Should prefer React Query/SWR for caching
"""

import json
import re
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
BASELINE_FILE = ROOT / ".claude" / "provenance" / "perf_baseline.json"

# Auto-detect build output directories
BUILD_DIRS = [
    ROOT / ".next",              # Next.js
    ROOT / "frontend" / ".next", # Next.js in frontend/
    ROOT / "dist",               # Vite, Webpack, etc.
    ROOT / "build",              # CRA, other bundlers
    ROOT / "out",                # Next.js static export
]

# ── Static Anti-Pattern Checks ──────────────────────────────────────

PERF_PATTERNS: list[tuple[str, str, str, str]] = [
    # (pattern_regex, file_glob, severity, description)
    (
        r'for\s+\w+\s+in\s+\w+.*?:\s*\n\s+.*?\.(query|execute|get|fetch|find)',
        "**/*.py",
        "medium",
        "Potential N+1: database access inside a loop",
    ),
    (
        r'\.all\(\)\s*$',
        "**/repositories/**/*.py",
        "low",
        "Unbounded .all() query — consider pagination or LIMIT",
    ),
    (
        r'import\s+pandas|from\s+pandas\s+import',
        "**/api/**/*.py",
        "medium",
        "Heavy library (pandas) imported in API handler — use in service layer",
    ),
    (
        r'import\s+numpy|from\s+numpy\s+import',
        "**/api/**/*.py",
        "medium",
        "Heavy library (numpy) imported in API handler — use in service layer",
    ),
    (
        r'selectinload\([^)]*\.\w+\)',
        "**/repositories/**/*.py",
        "low",
        "Verify selectinload target is a relationship, not a column",
    ),
    (
        r'useEffect\(\s*\(\)\s*=>\s*\{[^}]*fetch',
        "**/*.tsx",
        "low",
        "Fetch in useEffect — prefer React Query/SWR for caching and dedup",
    ),
]


def get_staged_files() -> list[str]:
    result = subprocess.run(
        ["git", "diff", "--cached", "--name-only"],
        capture_output=True, text=True, cwd=ROOT,
    )
    return [f for f in result.stdout.strip().split("\n") if f.strip()]


def check_static_patterns(files: list[str]) -> list[dict]:
    findings = []
    for filepath in files:
        full_path = ROOT / filepath
        if not full_path.exists() or not full_path.is_file():
            continue
        try:
            content = full_path.read_text(encoding="utf-8")
        except (UnicodeDecodeError, PermissionError):
            continue
        for pattern, file_glob, severity, description in PERF_PATTERNS:
            glob_re = file_glob.replace("**", ".*").replace("*", "[^/]*")
            if not re.search(glob_re, filepath):
                continue
            for match in re.finditer(pattern, content, re.MULTILINE):
                line_num = content[:match.start()].count("\n") + 1
                findings.append({
                    "severity": severity,
                    "file": filepath,
                    "line": line_num,
                    "description": description,
                    "match": match.group(0)[:80],
                })
    return findings


# ── Bundle Size Check ────────────────────────────────────────────────

def find_build_dir() -> Path | None:
    for d in BUILD_DIRS:
        if d.exists():
            return d
    return None


def check_bundle_size() -> dict | None:
    build_dir = find_build_dir()
    if not build_dir:
        print("No build directory found. Run your build command first.", file=sys.stderr)
        return None

    # Count JS files and total size
    total_size = 0
    js_count = 0
    for js_file in build_dir.rglob("*.js"):
        total_size += js_file.stat().st_size
        js_count += 1

    return {
        "build_dir": str(build_dir.relative_to(ROOT)),
        "js_files": js_count,
        "total_js_kb": round(total_size / 1024, 1),
    }


def save_baseline() -> None:
    bundle = check_bundle_size()
    baseline = {
        "timestamp": subprocess.run(
            ["date", "-Iseconds"], capture_output=True, text=True
        ).stdout.strip(),
        "bundle": bundle,
    }
    BASELINE_FILE.parent.mkdir(parents=True, exist_ok=True)
    BASELINE_FILE.write_text(json.dumps(baseline, indent=2, ensure_ascii=False), encoding="utf-8")
    print(f"Baseline saved to {BASELINE_FILE}")
    if bundle:
        print(f"  Bundle: {bundle['total_js_kb']} KB ({bundle['js_files']} JS files)")


def compare_baseline() -> None:
    if not BASELINE_FILE.exists():
        print("No baseline found. Run --baseline first.", file=sys.stderr)
        return
    baseline = json.loads(BASELINE_FILE.read_text(encoding="utf-8"))
    current_bundle = check_bundle_size()
    if not current_bundle or not baseline.get("bundle"):
        print("Cannot compare — missing bundle data.")
        return
    base_kb = baseline["bundle"]["total_js_kb"]
    curr_kb = current_bundle["total_js_kb"]
    delta = curr_kb - base_kb
    pct = (delta / base_kb * 100) if base_kb > 0 else 0
    print(f"Bundle Size Comparison:")
    print(f"  Baseline: {base_kb} KB")
    print(f"  Current:  {curr_kb} KB")
    print(f"  Delta:    {delta:+.1f} KB ({pct:+.1f}%)")
    if pct > 15:
        print(f"\n  WARNING: Bundle size increased by {pct:.1f}% (threshold: 15%)", file=sys.stderr)
    elif pct > 10:
        print(f"\n  INFO: Bundle size increased by {pct:.1f}%", file=sys.stderr)


def main() -> int:
    if "--baseline" in sys.argv:
        save_baseline()
        return 0
    if "--compare" in sys.argv:
        compare_baseline()
        return 0
    if "--bundle" in sys.argv:
        bundle = check_bundle_size()
        if bundle:
            print(f"Bundle: {bundle['total_js_kb']} KB ({bundle['js_files']} JS files in {bundle['build_dir']})")
        return 0

    # Default: static pattern check on staged files
    files = get_staged_files()
    if not files:
        return 0
    findings = check_static_patterns(files)
    if not findings:
        return 0
    print("PERF ADVISORY:", file=sys.stderr)
    for f in findings:
        print(f"  [{f['severity'].upper()}] {f['file']}:{f['line']}: {f['description']}", file=sys.stderr)
    return 0  # Advisory only


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