"""Limits monitoring — track Claude Code environment variable effectiveness.

Monitors 6 tuned Claude Code env vars that control agent performance:
  - BASH_MAX_OUTPUT_LENGTH (default: 30000)
  - CLAUDE_CODE_MAX_OUTPUT_TOKENS (default: 8192)
  - CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS (default: 10000)
  - CLAUDE_AUTOCOMPACT_PCT_OVERRIDE (default: 95)
  - CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY (default: 10)
  - BASH_MAX_TIMEOUT_MS (default: 120000)

Telemetry files:
  ~/.greymatter/limits_telemetry.jsonl  — session-start snapshots
  ~/.greymatter/limits_events.jsonl     — compaction + truncation events

The report aggregates these into actionable recommendations.
"""

from __future__ import annotations

import json
import os
import socket
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any

# ── Tracked limits with Claude Code defaults ────────────────────────
TRACKED_LIMITS: dict[str, dict[str, Any]] = {
    "BASH_MAX_OUTPUT_LENGTH": {
        "default": 30000,
        "description": "Max bash output characters captured",
        "unit": "chars",
    },
    "CLAUDE_CODE_MAX_OUTPUT_TOKENS": {
        "default": 8192,
        "description": "Max output tokens per response",
        "unit": "tokens",
    },
    "CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS": {
        "default": 10000,
        "description": "Max tokens for file reads",
        "unit": "tokens",
    },
    "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE": {
        "default": 95,
        "description": "Context % threshold for auto-compaction",
        "unit": "percent",
    },
    "CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY": {
        "default": 10,
        "description": "Max concurrent tool calls",
        "unit": "count",
    },
    "BASH_MAX_TIMEOUT_MS": {
        "default": 120000,
        "description": "Bash command timeout",
        "unit": "ms",
    },
}

GM_HOME = Path(os.environ.get("GM_HOME", os.path.expanduser("~/.greymatter")))
TELEMETRY_PATH = GM_HOME / "limits_telemetry.jsonl"
EVENTS_PATH = GM_HOME / "limits_events.jsonl"
MAX_TELEMETRY_BYTES = 1_048_576  # 1MB rotation threshold


# ── Snapshot current limit values ───────────────────────────────────

def current_limits() -> dict[str, Any]:
    """Read current limit values from the environment.

    Returns a dict mapping each env var name to its current value
    (as int) or the string 'default' if not set.
    """
    result: dict[str, Any] = {}
    for var, meta in TRACKED_LIMITS.items():
        raw = os.environ.get(var)
        if raw is not None:
            try:
                result[var] = int(raw)
            except ValueError:
                result[var] = raw
        else:
            result[var] = "default"
    return result


def tuned_env_vars() -> dict[str, str]:
    """Return only the limits that are explicitly set in the environment.

    Used by coordinators to propagate tuned values to spawned agents.
    Only includes vars that differ from defaults.
    """
    env: dict[str, str] = {}
    for var in TRACKED_LIMITS:
        val = os.environ.get(var)
        if val is not None:
            env[var] = val
    return env


# ── Telemetry file I/O ──────────────────────────────────────────────

def _rotate_if_needed(path: Path) -> None:
    """Rotate a JSONL file if it exceeds MAX_TELEMETRY_BYTES."""
    try:
        if path.exists() and path.stat().st_size > MAX_TELEMETRY_BYTES:
            rotated = path.with_suffix(".jsonl.old")
            if rotated.exists():
                rotated.unlink()
            path.rename(rotated)
    except OSError:
        pass


def log_session_limits(session_id: str = "") -> dict[str, Any]:
    """Log current limit values at session start.

    Returns the record that was logged, for testing.
    """
    record = {
        "timestamp": datetime.now().isoformat(),
        "event": "session_start",
        "session_id": session_id,
        "hostname": socket.gethostname(),
        "limits": current_limits(),
    }
    try:
        GM_HOME.mkdir(parents=True, exist_ok=True)
        _rotate_if_needed(TELEMETRY_PATH)
        with open(TELEMETRY_PATH, "a") as f:
            f.write(json.dumps(record, separators=(",", ":")) + "\n")
    except OSError:
        pass
    return record


def log_event(
    event_type: str,
    details: dict[str, Any] | None = None,
    session_id: str = "",
) -> dict[str, Any]:
    """Log a limits-related event (compaction, truncation, etc.).

    Event types:
        compaction_triggered — context compaction fired
        bash_truncated      — bash output hit the length limit
        file_read_truncated — file read hit the token limit
    """
    record = {
        "timestamp": datetime.now().isoformat(),
        "event": event_type,
        "session_id": session_id,
        **(details or {}),
    }
    try:
        GM_HOME.mkdir(parents=True, exist_ok=True)
        _rotate_if_needed(EVENTS_PATH)
        with open(EVENTS_PATH, "a") as f:
            f.write(json.dumps(record, separators=(",", ":")) + "\n")
    except OSError:
        pass
    return record


def read_telemetry(
    path: Path | None = None,
    max_age_days: int = 30,
) -> list[dict[str, Any]]:
    """Read telemetry records from a JSONL file, filtered by age."""
    target = path or TELEMETRY_PATH
    cutoff = (datetime.now() - timedelta(days=max_age_days)).isoformat()
    records: list[dict[str, Any]] = []
    try:
        if not target.exists():
            return []
        with open(target) as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                try:
                    rec = json.loads(line)
                    if rec.get("timestamp", "") >= cutoff:
                        records.append(rec)
                except json.JSONDecodeError:
                    continue
    except OSError:
        pass
    return records


def read_events(max_age_days: int = 30) -> list[dict[str, Any]]:
    """Read limits event records, filtered by age."""
    return read_telemetry(EVENTS_PATH, max_age_days)


# ── Report generation ──────────────────────────────────────────────

def _recommendation(
    var: str,
    current_val: Any,
    default_val: int,
    events: list[dict[str, Any]],
) -> dict[str, str]:
    """Generate a recommendation for a single limit setting."""
    if current_val == "default":
        effective = default_val
        is_tuned = False
    else:
        effective = int(current_val) if not isinstance(current_val, int) else current_val
        is_tuned = True

    rec: dict[str, str] = {
        "current": str(effective),
        "default": str(default_val),
        "tuned": "yes" if is_tuned else "no",
    }

    if var == "BASH_MAX_OUTPUT_LENGTH":
        truncations = [e for e in events if e.get("event") == "bash_truncated"]
        rec["truncation_events"] = str(len(truncations))
        if len(truncations) > 10:
            rec["recommendation"] = "increase"
            rec["reason"] = f"{len(truncations)} truncation events — agents are losing output data"
        elif len(truncations) == 0 and effective > default_val * 2:
            rec["recommendation"] = "keep"
            rec["reason"] = "No truncations — setting is effective (no waste: memory is not allocated until used)"
        else:
            rec["recommendation"] = "keep"
            rec["reason"] = "Current setting is working well"

    elif var == "CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS":
        truncations = [e for e in events if e.get("event") == "file_read_truncated"]
        rec["truncation_events"] = str(len(truncations))
        if len(truncations) > 5:
            rec["recommendation"] = "increase"
            rec["reason"] = f"{len(truncations)} file read truncations — agents may miss code context"
        else:
            rec["recommendation"] = "keep"
            rec["reason"] = "File reads completing without truncation"

    elif var == "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE":
        compactions = [e for e in events if e.get("event") == "compaction_triggered"]
        rec["compaction_events"] = str(len(compactions))
        if len(compactions) > 20:
            rec["recommendation"] = "decrease"
            rec["reason"] = f"{len(compactions)} compactions — context filling fast, compact earlier to preserve quality"
        elif len(compactions) == 0:
            rec["recommendation"] = "keep"
            rec["reason"] = "No compactions needed — sessions staying within context window"
        else:
            rec["recommendation"] = "keep"
            rec["reason"] = f"{len(compactions)} compactions — frequency looks healthy"

    elif var == "CLAUDE_CODE_MAX_OUTPUT_TOKENS":
        rec["recommendation"] = "keep"
        rec["reason"] = "Output token limit is not directly observable in hooks — keep current setting"

    elif var == "CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY":
        rec["recommendation"] = "keep"
        rec["reason"] = "Concurrency limit rarely needs adjustment — keep current setting"

    elif var == "BASH_MAX_TIMEOUT_MS":
        rec["recommendation"] = "keep"
        rec["reason"] = "Timeout provides safety margin for long builds — keep current setting"

    else:
        rec["recommendation"] = "keep"
        rec["reason"] = "No specific data to drive a change"

    return rec


def build_limits_report(max_age_days: int = 30) -> dict[str, Any]:
    """Build the full limits monitoring report.

    Returns a dict suitable for JSON serialization with:
        - current_limits: snapshot of all 6 env var values
        - compaction_history: summary of compaction events
        - truncation_rates: bash and file read truncation counts
        - recommendations: per-setting recommendation with rationale
        - session_count: number of sessions in the reporting period
    """
    limits = current_limits()
    telemetry = read_telemetry(max_age_days=max_age_days)
    events = read_events(max_age_days=max_age_days)

    # Session count
    sessions = [r for r in telemetry if r.get("event") == "session_start"]

    # Compaction history
    compactions = [e for e in events if e.get("event") == "compaction_triggered"]
    compaction_summary: dict[str, Any] = {
        "total_compactions": len(compactions),
        "reporting_period_days": max_age_days,
    }
    if compactions:
        # Average context bytes at compaction
        bytes_at_compaction = [
            e.get("estimated_context_bytes", 0)
            for e in compactions
            if e.get("estimated_context_bytes")
        ]
        if bytes_at_compaction:
            compaction_summary["avg_context_bytes_at_trigger"] = int(
                sum(bytes_at_compaction) / len(bytes_at_compaction)
            )
        # Compaction numbers (sequential count per session)
        numbers = [e.get("compaction_number", 0) for e in compactions]
        if numbers:
            compaction_summary["max_compaction_number"] = max(numbers)
            compaction_summary["avg_compaction_number"] = round(
                sum(numbers) / len(numbers), 1
            )

    # Truncation rates
    bash_truncations = [e for e in events if e.get("event") == "bash_truncated"]
    file_truncations = [e for e in events if e.get("event") == "file_read_truncated"]

    truncation_rates: dict[str, Any] = {
        "bash_output_truncations": len(bash_truncations),
        "file_read_truncations": len(file_truncations),
    }
    if bash_truncations:
        avg_output = sum(e.get("output_length", 0) for e in bash_truncations) / len(
            bash_truncations
        )
        truncation_rates["avg_bash_output_at_truncation"] = int(avg_output)
    if file_truncations:
        avg_tokens = sum(e.get("estimated_tokens", 0) for e in file_truncations) / len(
            file_truncations
        )
        truncation_rates["avg_file_tokens_at_truncation"] = int(avg_tokens)

    # Per-setting recommendations
    recommendations: dict[str, dict[str, str]] = {}
    for var, meta in TRACKED_LIMITS.items():
        recommendations[var] = _recommendation(
            var, limits[var], meta["default"], events
        )
        recommendations[var]["description"] = meta["description"]

    return {
        "report_generated": datetime.now().isoformat(),
        "reporting_period_days": max_age_days,
        "session_count": len(sessions),
        "current_limits": limits,
        "compaction_history": compaction_summary,
        "truncation_rates": truncation_rates,
        "recommendations": recommendations,
    }
