#!/usr/bin/env python3
"""
aria-run-log.py — query the structured run-record log written by aria-run-wrapper.sh.

Usage:
  aria-run-log.py                          # last 20 records
  aria-run-log.py --service aria-git-sync  # filter by service
  aria-run-log.py --since 24h              # last 24 hours
  aria-run-log.py --since 7d --status fail # last week's failures
  aria-run-log.py --tail                   # follow mode
  aria-run-log.py --summary                # aggregate per service

JSONL schema is defined in aria-run-wrapper.sh.
"""

from __future__ import annotations

import argparse
import json
import re
import sys
import time
from collections import Counter, defaultdict
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Iterable, Optional

LOG_FILE_DEFAULT = "/var/log/aria-runs.jsonl"


def parse_since(s: str) -> Optional[datetime]:
    """Accept '24h', '7d', '30m', '2026-05-10', or ISO datetime."""
    if not s:
        return None
    m = re.fullmatch(r"(\d+)([smhd])", s.strip())
    if m:
        n, unit = int(m.group(1)), m.group(2)
        delta = {"s": timedelta(seconds=n), "m": timedelta(minutes=n), "h": timedelta(hours=n), "d": timedelta(days=n)}[unit]
        return datetime.now(timezone.utc) - delta
    try:
        return datetime.fromisoformat(s.replace("Z", "+00:00")).astimezone(timezone.utc)
    except ValueError:
        raise SystemExit(f"unparseable --since: {s!r}")


def iter_records(path: Path) -> Iterable[dict]:
    if not path.is_file():
        return
    with path.open(encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                yield json.loads(line)
            except json.JSONDecodeError:
                continue


def filter_records(
    records: Iterable[dict],
    service: Optional[str],
    status: Optional[str],
    since: Optional[datetime],
) -> list[dict]:
    out = []
    for r in records:
        if service and r.get("service") != service:
            continue
        if status and r.get("status") != status:
            continue
        if since:
            try:
                ts = datetime.fromisoformat(r["started_at"].replace("Z", "+00:00"))
            except (KeyError, ValueError):
                continue
            if ts < since:
                continue
        out.append(r)
    return out


def fmt_record(r: dict) -> str:
    flag = {"ok": "✓", "fail": "✗"}.get(r.get("status", ""), "?")
    started = r.get("started_at", "")[:19].replace("T", " ")
    return (
        f"{flag} {started}  {r.get('service','?'):<32}"
        f" {r.get('duration_s',0):>7.2f}s  exit={r.get('exit_code','?')}"
    )


def cmd_summary(records: list[dict]) -> str:
    by_service: dict[str, Counter] = defaultdict(Counter)
    durations: dict[str, list[float]] = defaultdict(list)
    for r in records:
        by_service[r.get("service", "?")][r.get("status", "?")] += 1
        if isinstance(r.get("duration_s"), (int, float)):
            durations[r.get("service", "?")].append(float(r["duration_s"]))

    lines = ["Service                             ok    fail   total   avg-dur"]
    for service in sorted(by_service):
        c = by_service[service]
        ok = c.get("ok", 0)
        fail = c.get("fail", 0)
        total = ok + fail
        ds = durations[service]
        avg = (sum(ds) / len(ds)) if ds else 0.0
        lines.append(f"{service:<35} {ok:>5}  {fail:>5}  {total:>5}  {avg:>7.2f}s")
    return "\n".join(lines)


def main() -> int:
    p = argparse.ArgumentParser(description="Query Aria's structured run-record log.")
    p.add_argument("--log", default=LOG_FILE_DEFAULT, help=f"path to JSONL log (default {LOG_FILE_DEFAULT})")
    p.add_argument("--service", help="filter by service name")
    p.add_argument("--status", choices=["ok", "fail"], help="filter by status")
    p.add_argument("--since", help="only records since (e.g. 24h, 7d, 2026-05-10)")
    p.add_argument("--limit", type=int, default=20, help="max records to show (default 20)")
    p.add_argument("--summary", action="store_true", help="aggregate counts per service")
    p.add_argument("--tail", action="store_true", help="follow mode: stream new records as appended")
    args = p.parse_args()

    path = Path(args.log)
    since = parse_since(args.since) if args.since else None

    if args.tail:
        # Simple tail-follow: re-stat the file every second.
        last_size = 0
        try:
            while True:
                if path.is_file():
                    size = path.stat().st_size
                    if size > last_size:
                        with path.open(encoding="utf-8") as f:
                            f.seek(last_size)
                            for line in f:
                                line = line.strip()
                                if not line:
                                    continue
                                try:
                                    r = json.loads(line)
                                except json.JSONDecodeError:
                                    continue
                                print(fmt_record(r), flush=True)
                        last_size = size
                time.sleep(1.0)
        except KeyboardInterrupt:
            return 0

    records = filter_records(iter_records(path), args.service, args.status, since)

    if args.summary:
        print(cmd_summary(records))
        return 0

    for r in records[-args.limit:]:
        print(fmt_record(r))

    if not records:
        print(f"(no records in {path})", file=sys.stderr)
    return 0


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