"""Publish auditor.

Two jobs:

1. Append a JSON line to a local audit log for every publish — ``{subject,
   bytes, ts, type}`` — so an operator can review what an agent has sent
   without ingesting the whole NATS stream.
2. Track per-subject publish rate over a 1-second sliding window and surface
   subjects that cross a configurable threshold. Useful for catching pub
   storms before they take out the bus.

The auditor is intentionally synchronous and lock-free — it's called from the
publish hot path, and the tradeoff (a torn ``trips`` list under aggressive
threading) is acceptable for a diagnostic aid.
"""
from __future__ import annotations

import json
import os
import time
from collections import deque
from datetime import datetime, timezone
from pathlib import Path


class PublishAuditor:
    def __init__(
        self,
        log_path: str | os.PathLike | None = None,
        default_rate_threshold: int = 500,
    ) -> None:
        self.log_path = Path(log_path) if log_path else None
        if self.log_path:
            self.log_path.parent.mkdir(parents=True, exist_ok=True)
        self.default_threshold = default_rate_threshold
        self._overrides: dict[str, int] = {}
        self._buckets: dict[str, deque[float]] = {}
        self.trips: list[tuple[str, int, float]] = []

    def set_threshold(self, subject: str, per_second: int) -> None:
        self._overrides[subject] = per_second

    def threshold_for(self, subject: str) -> int:
        return self._overrides.get(subject, self.default_threshold)

    def track(self, subject: str, body_bytes: int, envelope_type: str | None = None) -> None:
        now = time.monotonic()
        bucket = self._buckets.setdefault(subject, deque())
        bucket.append(now)
        while bucket and now - bucket[0] > 1.0:
            bucket.popleft()
        thr = self.threshold_for(subject)
        if len(bucket) > thr:
            self.trips.append((subject, len(bucket), now))

        if self.log_path:
            entry = {
                "ts": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
                "subject": subject,
                "bytes": body_bytes,
                "type": envelope_type,
            }
            try:
                with self.log_path.open("a", encoding="utf-8") as fh:
                    fh.write(json.dumps(entry, separators=(",", ":")) + "\n")
            except OSError:
                # never let audit logging break a publish
                pass

    def reset_trips(self) -> None:
        self.trips.clear()
