#!/usr/bin/env python3
"""
Log rotation: compress old logs, delete expired logs.
Run daily via cron or launchd to prevent disk bloat.

Usage:
  python3 ops/rotate_logs.py                    # dry-run (report only)
  python3 ops/rotate_logs.py --execute          # actually rotate

Rules:
  - Daily logs (*.YYYYMMDD.log) older than 7 days  → gzip compress
  - Daily logs (*.YYYYMMDD.log) older than 30 days → delete
  - NDJSON archives (*.ndjson) older than 2 days   → gzip compress
  - .ndjson.gz archives older than 60 days         → delete
  - Rotated runtimes (runtime.*.log) older than 1 day → gzip compress + delete original
"""

from __future__ import annotations

import argparse
import gzip
import os
import re
import time
from datetime import datetime, timedelta
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
LOGS_DIR = ROOT / "logs"

# Bytes → human readable
def _h(size: int) -> str:
    for unit in ("B", "KB", "MB", "GB"):
        if size < 1024:
            return f"{size:.0f}{unit}"
        size /= 1024
    return f"{size:.0f}TB"

def _age_days(path: Path) -> int:
    """Days since last modification."""
    mtime = os.path.getmtime(path)
    return int((time.time() - mtime) / 86400)

def _extract_date(filename: str) -> str | None:
    """Try to extract YYYYMMDD from filename."""
    m = re.search(r"(\d{8})", filename)
    return m.group(1) if m else None

def rotate(dry_run: bool = True) -> None:
    if not LOGS_DIR.exists():
        print("Logs directory not found:", LOGS_DIR)
        return

    actions: list[tuple[str, str, int]] = []  # (action, path, size)

    for f in sorted(LOGS_DIR.iterdir()):
        if not f.is_file():
            continue
        name = f.name
        size = f.stat().st_size
        age = _age_days(f)

        # Skip already compressed + small files
        if name.endswith(".gz"):
            if age > 60:
                actions.append(("DELETE_EXPIRED", str(f), size))
            continue

        if size < 1024:  # < 1KB, skip
            continue

        # Rotated runtimes (runtime.YYYYMMDD_HHMMSS.log)
        if name.startswith("runtime.") and name.endswith(".log"):
            date_part = _extract_date(name)
            if date_part:
                try:
                    dt = datetime.strptime(date_part, "%Y%m%d")
                    if dt < datetime.now() - timedelta(days=1):
                        actions.append(("COMPRESS_ROTATED", str(f), size))
                except ValueError:
                    pass
            continue

        # Daily logs (*.YYYYMMDD.log)
        if name.endswith(".log"):
            date_part = _extract_date(name)
            if date_part:
                if age > 30:
                    actions.append(("DELETE_EXPIRED", str(f), size))
                elif age > 7:
                    actions.append(("COMPRESS", str(f), size))
                continue

        # Live NDJSON → compress after 2 days idle
        if name.endswith(".ndjson") and name not in (
            "decision_ledger.ndjson",
            "trading_ledger.ndjson",
            "signal_ledger.ndjson",
            "risk_events.ndjson",
            "chat_tuning_audit.ndjson",
        ):
            if age > 2:
                actions.append(("COMPRESS", str(f), size))

    # ── Execute ──────────────────────────────────────────────
    total_saved = 0
    for action, path_str, size in actions:
        path = Path(path_str)
        label = "[DRY-RUN]" if dry_run else "[EXEC]"

        if action == "DELETE_EXPIRED":
            print(f"{label} DELETE  {path.name}  ({_h(size)})")
            if not dry_run:
                path.unlink(missing_ok=True)
                total_saved += size

        elif action in ("COMPRESS", "COMPRESS_ROTATED"):
            gz_path = path.with_suffix(path.suffix + ".gz")
            if not dry_run:
                with open(path, "rb") as f_in:
                    with gzip.open(gz_path, "wb", compresslevel=6) as f_out:
                        f_out.write(f_in.read())
                path.unlink()
                compressed_size = gz_path.stat().st_size
            else:
                compressed_size = int(size * 0.05)  # rough estimate
            saved = size - compressed_size
            total_saved += saved
            print(f"{label} GZIP   {path.name}  ({_h(size)} → ~{_h(compressed_size)}, saved {_h(saved)})")

    if not actions:
        print("Nothing to rotate — all logs within retention window.")
    elif dry_run:
        print(f"\nDRY-RUN: {len(actions)} files, would save ~{_h(total_saved)}")
        print("Run with --execute to apply.")
    else:
        print(f"\nDone: {len(actions)} files processed, freed {_h(total_saved)}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Rotate gold_autotrade logs")
    parser.add_argument("--execute", action="store_true", help="Actually rotate (default: dry-run)")
    args = parser.parse_args()
    rotate(dry_run=not args.execute)
