"""
API Governance — Rate limiting, tier-based quotas, abuse detection (Phase 35).

Production-grade API governance for the Brynq platform public API.
Complements the IP-level rate limiting in ``api_security.py`` with:
  - Token-bucket rate limiter (per API key, thread-safe)
  - Tier-based quotas (free / pro / enterprise)
  - Request logging to JSONL with usage analytics
  - Abuse detection (rapid-fire, high error rate, endpoint scanning)
  - API key suspension management

Storage layout:
  state/broker/governance/
    request_log.jsonl          — append-only request log
    suspensions/{key_hash}.json — suspended key metadata

All functions are sync (no async) for use from CLI and MCP tool wrappers.
In-memory state for rate limiting; file-based for logs and suspensions.
"""

import hashlib
import json
import threading
import time
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional

from brynq.server import BROKER_DIR

# ── Paths ──────────────────────────────────────────────────────────────────

GOVERNANCE_DIR = BROKER_DIR / "governance"
REQUEST_LOG_PATH = GOVERNANCE_DIR / "request_log.jsonl"
SUSPENSIONS_DIR = GOVERNANCE_DIR / "suspensions"

for _d in (GOVERNANCE_DIR, SUSPENSIONS_DIR):
    _d.mkdir(parents=True, exist_ok=True)

# ── Tier Definitions ──────────────────────────────────────────────────────

TIER_LIMITS: dict[str, dict[str, Any]] = {
    "free": {
        "requests_per_minute": 100,
        "requests_per_hour": 1000,
        "requests_per_day": 10000,
    },
    "pro": {
        "requests_per_minute": 500,
        "requests_per_hour": 5000,
        "requests_per_day": 50000,
    },
    "enterprise": {
        "requests_per_minute": 2000,
        "requests_per_hour": 20000,
        "requests_per_day": 0,  # 0 = unlimited
    },
}


# ── Token Bucket Rate Limiter ─────────────────────────────────────────────

class RateLimiter:
    """Thread-safe token-bucket rate limiter.

    Each key gets ``capacity`` tokens that refill at ``refill_rate`` per second.
    Each ``allow()`` call consumes one token if available.
    """

    def __init__(self, capacity: int = 100, refill_rate: float = 10.0):
        self.capacity = capacity
        self.refill_rate = refill_rate  # tokens per second
        self._buckets: dict[str, dict[str, float]] = {}
        self._lock = threading.Lock()

    def allow(self, key: str) -> tuple[bool, int, float]:
        """Check if a request for *key* is allowed.

        Returns:
            (allowed, remaining, retry_after)
            - allowed: True if the request should proceed.
            - remaining: tokens left after this request (0 if denied).
            - retry_after: seconds until next token (0.0 if allowed).
        """
        with self._lock:
            now = time.monotonic()
            bucket = self._buckets.get(key)

            if bucket is None:
                bucket = {"tokens": float(self.capacity), "last_refill": now}
                self._buckets[key] = bucket

            # Refill
            elapsed = now - bucket["last_refill"]
            bucket["tokens"] = min(
                float(self.capacity),
                bucket["tokens"] + elapsed * self.refill_rate,
            )
            bucket["last_refill"] = now

            if bucket["tokens"] >= 1.0:
                bucket["tokens"] -= 1.0
                remaining = int(bucket["tokens"])
                return True, remaining, 0.0
            else:
                # How long until we have 1 token?
                deficit = 1.0 - bucket["tokens"]
                retry_after = deficit / self.refill_rate if self.refill_rate > 0 else float("inf")
                return False, 0, round(retry_after, 3)

    def reset(self, key: str) -> None:
        """Remove a single key's bucket."""
        with self._lock:
            self._buckets.pop(key, None)

    def reset_all(self) -> None:
        """Clear every bucket (admin reset)."""
        with self._lock:
            self._buckets.clear()

    def peek(self, key: str) -> int:
        """Return current token count for *key* without consuming."""
        with self._lock:
            bucket = self._buckets.get(key)
            if bucket is None:
                return self.capacity
            now = time.monotonic()
            elapsed = now - bucket["last_refill"]
            current = min(
                float(self.capacity),
                bucket["tokens"] + elapsed * self.refill_rate,
            )
            return int(current)


# ── Per-window counters (minute / hour / day) ─────────────────────────────

class _WindowCounter:
    """Thread-safe sliding-window counter for quota enforcement."""

    def __init__(self, window_seconds: int):
        self.window_seconds = window_seconds
        self._hits: dict[str, list[float]] = {}
        self._lock = threading.Lock()

    def record(self, key: str) -> int:
        """Record a hit and return the count in the current window."""
        with self._lock:
            now = time.monotonic()
            cutoff = now - self.window_seconds
            hits = self._hits.setdefault(key, [])
            hits[:] = [t for t in hits if t > cutoff]
            hits.append(now)
            return len(hits)

    def count(self, key: str) -> int:
        """Current count without recording."""
        with self._lock:
            now = time.monotonic()
            cutoff = now - self.window_seconds
            hits = self._hits.get(key, [])
            return sum(1 for t in hits if t > cutoff)

    def reset(self, key: str) -> None:
        with self._lock:
            self._hits.pop(key, None)

    def reset_all(self) -> None:
        with self._lock:
            self._hits.clear()


# ── Global limiters ───────────────────────────────────────────────────────

_minute_counter = _WindowCounter(60)
_hour_counter = _WindowCounter(3600)
_day_counter = _WindowCounter(86400)

# Per-key token bucket — capacity sized for free tier by default.
# ``check_rate_limit`` selects the correct tier limits dynamically.
_token_bucket = RateLimiter(capacity=100, refill_rate=10.0)


# ── Tier-based Quota Check ────────────────────────────────────────────────

def check_rate_limit(
    api_key: str,
    tier: str = "free",
) -> tuple[bool, dict[str, str]]:
    """Check whether *api_key* is within its tier's rate limits.

    Returns:
        (allowed, headers_dict) where *headers_dict* contains standard
        rate-limit response headers suitable for adding to the HTTP response.
    """
    limits = TIER_LIMITS.get(tier, TIER_LIMITS["free"])

    per_min = limits["requests_per_minute"]
    per_hour = limits["requests_per_hour"]
    per_day = limits["requests_per_day"]  # 0 = unlimited

    minute_count = _minute_counter.count(api_key)
    hour_count = _hour_counter.count(api_key)
    day_count = _day_counter.count(api_key)

    # Determine the tightest constraint
    allowed = True
    retry_after = 0.0
    limit_used = per_min
    remaining = per_min - minute_count

    if minute_count >= per_min:
        allowed = False
        retry_after = 60.0
        remaining = 0
    elif hour_count >= per_hour:
        allowed = False
        retry_after = 3600.0
        remaining = 0
        limit_used = per_hour
    elif per_day > 0 and day_count >= per_day:
        allowed = False
        retry_after = 86400.0
        remaining = 0
        limit_used = per_day

    if allowed:
        _minute_counter.record(api_key)
        _hour_counter.record(api_key)
        _day_counter.record(api_key)
        remaining = per_min - _minute_counter.count(api_key)

    now_ts = time.time()
    reset_at = int(now_ts + 60)  # next minute boundary

    headers: dict[str, str] = {
        "X-RateLimit-Limit": str(per_min),
        "X-RateLimit-Remaining": str(max(0, remaining)),
        "X-RateLimit-Reset": str(reset_at),
    }
    if not allowed:
        headers["Retry-After"] = str(int(retry_after))

    return allowed, headers


# ── Request Logging ───────────────────────────────────────────────────────

_log_lock = threading.Lock()


def log_request(
    api_key: str,
    endpoint: str,
    method: str,
    status_code: int,
    response_time_ms: float,
) -> dict:
    """Append a request record to the JSONL log. Returns the record."""
    record = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "api_key": api_key,
        "endpoint": endpoint,
        "method": method.upper(),
        "status_code": status_code,
        "response_time_ms": round(response_time_ms, 2),
    }
    with _log_lock:
        GOVERNANCE_DIR.mkdir(parents=True, exist_ok=True)
        with REQUEST_LOG_PATH.open("a", encoding="utf-8") as f:
            f.write(json.dumps(record, default=str) + "\n")
    return record


def _read_request_log() -> list[dict]:
    """Read all request log entries."""
    if not REQUEST_LOG_PATH.exists():
        return []
    entries: list[dict] = []
    for line in REQUEST_LOG_PATH.read_text("utf-8").strip().splitlines():
        try:
            entries.append(json.loads(line))
        except json.JSONDecodeError:
            continue
    return entries


def get_usage_stats(api_key: str, hours: int = 24) -> dict:
    """Return usage statistics for *api_key* over the last *hours* hours.

    Returns dict with: total_requests, avg_response_time_ms, error_rate,
    top_endpoints, status_codes.
    """
    cutoff = datetime.now(timezone.utc).timestamp() - hours * 3600
    entries = _read_request_log()

    filtered: list[dict] = []
    for e in entries:
        if e.get("api_key") != api_key:
            continue
        try:
            ts = datetime.fromisoformat(e["ts"]).timestamp()
        except (KeyError, ValueError):
            continue
        if ts >= cutoff:
            filtered.append(e)

    if not filtered:
        return {
            "total_requests": 0,
            "avg_response_time_ms": 0.0,
            "error_rate": 0.0,
            "top_endpoints": [],
            "status_codes": {},
        }

    total = len(filtered)
    avg_rt = sum(e.get("response_time_ms", 0) for e in filtered) / total
    error_count = sum(1 for e in filtered if e.get("status_code", 200) >= 400)
    error_rate = error_count / total

    endpoint_counts = Counter(e.get("endpoint", "unknown") for e in filtered)
    top_endpoints = [
        {"endpoint": ep, "count": c}
        for ep, c in endpoint_counts.most_common(10)
    ]

    status_counts: dict[str, int] = {}
    for e in filtered:
        sc = str(e.get("status_code", 0))
        status_counts[sc] = status_counts.get(sc, 0) + 1

    return {
        "total_requests": total,
        "avg_response_time_ms": round(avg_rt, 2),
        "error_rate": round(error_rate, 4),
        "top_endpoints": top_endpoints,
        "status_codes": status_counts,
    }


# ── Abuse Detection ───────────────────────────────────────────────────────

def check_abuse(api_key: str) -> tuple[bool, str, float]:
    """Analyse recent request history for *api_key* and flag abuse.

    Checks for:
      1. Rapid-fire: >50 requests in last 10 seconds.
      2. High error rate: >50% 4xx/5xx in last 100 requests.
      3. Endpoint scanning: >20 distinct endpoints in last 30 seconds.

    Returns:
        (is_abusive, reason, confidence)  — confidence in [0.0, 1.0].
    """
    entries = _read_request_log()
    key_entries = [e for e in entries if e.get("api_key") == api_key]

    if not key_entries:
        return False, "", 0.0

    now = datetime.now(timezone.utc).timestamp()

    # --- 1. Rapid-fire ---
    recent_10s = []
    for e in key_entries:
        try:
            ts = datetime.fromisoformat(e["ts"]).timestamp()
        except (KeyError, ValueError):
            continue
        if now - ts <= 10:
            recent_10s.append(e)

    if len(recent_10s) > 50:
        confidence = min(1.0, len(recent_10s) / 100.0)
        return True, f"Rapid-fire: {len(recent_10s)} requests in 10s", confidence

    # --- 2. High error rate ---
    last_100 = key_entries[-100:]
    if len(last_100) >= 10:
        error_count = sum(1 for e in last_100 if e.get("status_code", 200) >= 400)
        error_rate = error_count / len(last_100)
        if error_rate > 0.5:
            return True, f"High error rate: {error_rate:.0%} in last {len(last_100)} requests", error_rate

    # --- 3. Endpoint scanning ---
    recent_30s = []
    for e in key_entries:
        try:
            ts = datetime.fromisoformat(e["ts"]).timestamp()
        except (KeyError, ValueError):
            continue
        if now - ts <= 30:
            recent_30s.append(e)

    if recent_30s:
        distinct_endpoints = len(set(e.get("endpoint", "") for e in recent_30s))
        if distinct_endpoints > 20:
            confidence = min(1.0, distinct_endpoints / 40.0)
            return True, f"Endpoint scanning: {distinct_endpoints} distinct endpoints in 30s", confidence

    return False, "", 0.0


# ── API Key Management Helpers ────────────────────────────────────────────

def _key_hash(api_key: str) -> str:
    """Deterministic short hash of an API key for filenames."""
    return hashlib.sha256(api_key.encode("utf-8")).hexdigest()[:16]


def get_key_info(api_key: str) -> Optional[dict]:
    """Return tier, owner, created, and usage stats for *api_key*.

    Scans the api_keys directory (same layout as ``api_server.py``).
    Returns None if the key is not found.
    """
    keys_dir = BROKER_DIR / "api_keys"
    if not keys_dir.exists():
        return None

    for f in keys_dir.glob("*.json"):
        try:
            data = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        if data.get("api_key") == api_key:
            stats = get_usage_stats(api_key, hours=24)
            return {
                "api_key": api_key,
                "tier": data.get("tier", "free"),
                "owner": data.get("name", data.get("session_name", "unknown")),
                "created": data.get("created", "unknown"),
                "usage_24h": stats,
            }
    return None


def suspend_key(api_key: str, reason: str) -> dict:
    """Temporarily suspend an API key. Returns the suspension record."""
    SUSPENSIONS_DIR.mkdir(parents=True, exist_ok=True)
    record = {
        "api_key": api_key,
        "reason": reason,
        "suspended_at": datetime.now(timezone.utc).isoformat(),
    }
    path = SUSPENSIONS_DIR / f"{_key_hash(api_key)}.json"
    path.write_text(json.dumps(record, indent=2, default=str), encoding="utf-8")
    return record


def unsuspend_key(api_key: str) -> bool:
    """Remove a suspension. Returns True if one existed."""
    path = SUSPENSIONS_DIR / f"{_key_hash(api_key)}.json"
    if path.exists():
        path.unlink()
        return True
    return False


def is_suspended(api_key: str) -> bool:
    """Check whether *api_key* is currently suspended."""
    path = SUSPENSIONS_DIR / f"{_key_hash(api_key)}.json"
    return path.exists()


# ── Admin Utilities ───────────────────────────────────────────────────────

def reset_quotas() -> dict:
    """Clear all in-memory rate-limit buckets (admin use).

    Returns summary of what was reset.
    """
    _token_bucket.reset_all()
    _minute_counter.reset_all()
    _hour_counter.reset_all()
    _day_counter.reset_all()
    return {"status": "ok", "message": "All rate-limit buckets cleared"}


def get_all_usage(hours: int = 24) -> dict:
    """Platform-wide usage summary over the last *hours* hours.

    Returns: total_requests, unique_keys, avg_response_time_ms, error_rate,
    top_keys, top_endpoints.
    """
    cutoff = datetime.now(timezone.utc).timestamp() - hours * 3600
    entries = _read_request_log()

    filtered: list[dict] = []
    for e in entries:
        try:
            ts = datetime.fromisoformat(e["ts"]).timestamp()
        except (KeyError, ValueError):
            continue
        if ts >= cutoff:
            filtered.append(e)

    if not filtered:
        return {
            "total_requests": 0,
            "unique_keys": 0,
            "avg_response_time_ms": 0.0,
            "error_rate": 0.0,
            "top_keys": [],
            "top_endpoints": [],
        }

    total = len(filtered)
    unique_keys = len(set(e.get("api_key", "") for e in filtered))
    avg_rt = sum(e.get("response_time_ms", 0) for e in filtered) / total
    error_count = sum(1 for e in filtered if e.get("status_code", 200) >= 400)
    error_rate = error_count / total

    key_counts = Counter(e.get("api_key", "unknown") for e in filtered)
    top_keys = [
        {"api_key": k, "count": c}
        for k, c in key_counts.most_common(10)
    ]

    endpoint_counts = Counter(e.get("endpoint", "unknown") for e in filtered)
    top_endpoints = [
        {"endpoint": ep, "count": c}
        for ep, c in endpoint_counts.most_common(10)
    ]

    return {
        "total_requests": total,
        "unique_keys": unique_keys,
        "avg_response_time_ms": round(avg_rt, 2),
        "error_rate": round(error_rate, 4),
        "top_keys": top_keys,
        "top_endpoints": top_endpoints,
    }
