"""
Brynq Agent Analytics / BI — Performance intelligence for AI agents.

Provides throughput metrics, task analytics, response-time tracking,
activity heatmaps, leaderboards, and trend detection.  All data is
derived from the existing JSONL channel logs and task JSON files —
no external dependencies required.

Storage:
  state/broker/analytics/
    snapshots/{date}.json — daily metric snapshots
    events.jsonl          — analytics events log
"""

import json
import os
from collections import defaultdict
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Any, Optional

from brynq.server import (
    BROKER_DIR,
    CHANNELS_DIR,
    SESSIONS_DIR,
    SESSION_ID,
    _read_jsonl_sync,
    _write_json_sync,
    _append_jsonl_sync,
)

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

ANALYTICS_DIR = BROKER_DIR / "analytics"
SNAPSHOTS_DIR = ANALYTICS_DIR / "snapshots"
EVENTS_LOG = ANALYTICS_DIR / "events.jsonl"

for _d in (ANALYTICS_DIR, SNAPSHOTS_DIR):
    _d.mkdir(parents=True, exist_ok=True)

# ── Internal constants ─────────────────────────────────────────────────────

TASKS_DIR = BROKER_DIR / "tasks"

_SYSTEM_CHANNELS = frozenset({"_system"})


# ── Helpers ────────────────────────────────────────────────────────────────


def _parse_iso(ts: str) -> Optional[datetime]:
    """Parse an ISO-8601 timestamp string into a timezone-aware datetime."""
    if not ts:
        return None
    try:
        dt = datetime.fromisoformat(ts)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        return dt
    except (ValueError, TypeError):
        return None


def _now_utc() -> datetime:
    return datetime.now(timezone.utc)


def _load_all_channel_messages(hours: Optional[float] = None) -> list[dict]:
    """Load messages from all channel JSONL files.

    If *hours* is given, only messages within that many hours of now are
    returned.  Otherwise every message is returned.
    """
    cutoff = _now_utc() - timedelta(hours=hours) if hours else None
    messages: list[dict] = []
    if not CHANNELS_DIR.exists():
        return messages
    for path in CHANNELS_DIR.glob("*.jsonl"):
        channel = path.stem
        for entry in _read_jsonl_sync(path):
            entry.setdefault("channel", channel)
            if cutoff is not None:
                ts = _parse_iso(entry.get("timestamp", ""))
                if ts is None or ts < cutoff:
                    continue
            messages.append(entry)
    return messages


def _load_all_tasks() -> list[dict]:
    """Load every task JSON file from the tasks directory."""
    tasks: list[dict] = []
    if not TASKS_DIR.exists():
        return tasks
    for path in TASKS_DIR.glob("*.json"):
        try:
            data = json.loads(path.read_text("utf-8"))
            tasks.append(data)
        except (json.JSONDecodeError, OSError):
            continue
    return tasks


def _load_all_sessions() -> list[dict]:
    """Load every session JSON file."""
    sessions: list[dict] = []
    if not SESSIONS_DIR.exists():
        return sessions
    for path in SESSIONS_DIR.glob("*.json"):
        try:
            data = json.loads(path.read_text("utf-8"))
            sessions.append(data)
        except (json.JSONDecodeError, OSError):
            continue
    return sessions


def _log_event(event_type: str, data: dict) -> None:
    """Append an analytics event to the events log."""
    entry = {
        "timestamp": _now_utc().isoformat(),
        "event": event_type,
        "session_id": SESSION_ID,
        **data,
    }
    _append_jsonl_sync(EVENTS_LOG, entry)


# ── Public API ─────────────────────────────────────────────────────────────


def get_message_stats(hours: int = 24) -> dict[str, Any]:
    """Message throughput statistics for the last *hours* hours.

    Returns counts per channel, per agent (session_id), and a per-hour
    histogram for the entire window.
    """
    messages = _load_all_channel_messages(hours=hours)
    now = _now_utc()

    per_channel: dict[str, int] = defaultdict(int)
    per_agent: dict[str, int] = defaultdict(int)
    per_hour: dict[str, int] = defaultdict(int)

    for msg in messages:
        channel = msg.get("channel", "unknown")
        agent = msg.get("session_id", "unknown")
        per_channel[channel] += 1
        per_agent[agent] += 1

        ts = _parse_iso(msg.get("timestamp", ""))
        if ts is not None:
            # Bucket by hour label, e.g. "2026-03-16T14"
            hour_key = ts.strftime("%Y-%m-%dT%H")
            per_hour[hour_key] += 1

    total = len(messages)
    msgs_per_hour = round(total / max(hours, 1), 2)

    return {
        "window_hours": hours,
        "total_messages": total,
        "messages_per_hour": msgs_per_hour,
        "per_channel": dict(sorted(per_channel.items(), key=lambda x: -x[1])),
        "per_agent": dict(sorted(per_agent.items(), key=lambda x: -x[1])),
        "per_hour": dict(sorted(per_hour.items())),
    }


def get_task_stats() -> dict[str, Any]:
    """Task-level metrics: completion rate, avg time, success rate per agent.

    Scans every task file in state/broker/tasks/.
    """
    tasks = _load_all_tasks()
    total = len(tasks)
    if total == 0:
        return {
            "total_tasks": 0,
            "by_status": {},
            "completion_rate": 0.0,
            "success_rate": 0.0,
            "avg_completion_seconds": None,
            "per_agent": {},
        }

    by_status: dict[str, int] = defaultdict(int)
    completion_times: list[float] = []
    per_agent: dict[str, dict[str, Any]] = defaultdict(
        lambda: {"assigned": 0, "completed": 0, "failed": 0, "total_seconds": 0.0}
    )

    for task in tasks:
        status = task.get("status", "unknown")
        by_status[status] += 1

        assignee = task.get("assignee_session")
        if assignee:
            per_agent[assignee]["assigned"] += 1

        if status == "completed":
            if assignee:
                per_agent[assignee]["completed"] += 1
            accepted = _parse_iso(task.get("accepted_at", ""))
            completed = _parse_iso(task.get("completed_at", ""))
            if accepted and completed:
                delta = (completed - accepted).total_seconds()
                completion_times.append(delta)
                if assignee:
                    per_agent[assignee]["total_seconds"] += delta

        elif status == "failed":
            if assignee:
                per_agent[assignee]["failed"] += 1

    completed_count = by_status.get("completed", 0)
    failed_count = by_status.get("failed", 0)
    finished = completed_count + failed_count

    completion_rate = round(finished / total * 100, 1) if total else 0.0
    success_rate = round(completed_count / finished * 100, 1) if finished else 0.0
    avg_seconds = (
        round(sum(completion_times) / len(completion_times), 1)
        if completion_times
        else None
    )

    # Add per-agent averages
    agent_stats: dict[str, dict[str, Any]] = {}
    for agent_id, stats in per_agent.items():
        agent_stats[agent_id] = {
            "assigned": stats["assigned"],
            "completed": stats["completed"],
            "failed": stats["failed"],
            "success_rate": (
                round(stats["completed"] / max(stats["completed"] + stats["failed"], 1) * 100, 1)
            ),
            "avg_completion_seconds": (
                round(stats["total_seconds"] / stats["completed"], 1)
                if stats["completed"] > 0
                else None
            ),
        }

    return {
        "total_tasks": total,
        "by_status": dict(by_status),
        "completion_rate": completion_rate,
        "success_rate": success_rate,
        "avg_completion_seconds": avg_seconds,
        "per_agent": agent_stats,
    }


def get_leaderboard(metric: str = "tasks_completed", limit: int = 10) -> list[dict]:
    """Rank agents by a chosen metric.

    Supported metrics:
      - tasks_completed  — total completed tasks
      - success_rate     — completion success percentage
      - response_time    — average seconds to complete (lower is better)
      - messages_sent    — total messages posted

    Returns a list of dicts sorted best-first, capped at *limit*.
    """
    valid_metrics = {"tasks_completed", "success_rate", "response_time", "messages_sent"}
    if metric not in valid_metrics:
        raise ValueError(
            f"Invalid metric '{metric}'. Must be one of: {', '.join(sorted(valid_metrics))}"
        )

    if metric == "messages_sent":
        stats = get_message_stats(hours=720)  # ~30 days
        board = [
            {"agent": agent, "messages_sent": count}
            for agent, count in stats["per_agent"].items()
        ]
        board.sort(key=lambda x: -x["messages_sent"])
        for rank, entry in enumerate(board, 1):
            entry["rank"] = rank
        return board[:limit]

    # Task-based metrics
    task_stats = get_task_stats()
    agent_data = task_stats.get("per_agent", {})

    board: list[dict] = []
    for agent_id, info in agent_data.items():
        entry: dict[str, Any] = {"agent": agent_id, **info}
        board.append(entry)

    if metric == "tasks_completed":
        board.sort(key=lambda x: -x.get("completed", 0))
    elif metric == "success_rate":
        board.sort(key=lambda x: -x.get("success_rate", 0))
    elif metric == "response_time":
        # Lower is better; agents with None go to the end
        board.sort(
            key=lambda x: x.get("avg_completion_seconds") if x.get("avg_completion_seconds") is not None else float("inf")
        )

    for rank, entry in enumerate(board, 1):
        entry["rank"] = rank

    return board[:limit]


def get_agent_activity(session_id: str, hours: int = 24) -> dict[str, Any]:
    """Detailed activity breakdown for a single agent.

    Includes messages sent, tasks handled, hourly activity heatmap,
    and response-time stats.
    """
    messages = _load_all_channel_messages(hours=hours)
    agent_messages = [m for m in messages if m.get("session_id") == session_id]

    # Hourly heatmap (0..23)
    heatmap: dict[int, int] = defaultdict(int)
    channels_active: set[str] = set()
    for msg in agent_messages:
        ts = _parse_iso(msg.get("timestamp", ""))
        if ts:
            heatmap[ts.hour] += 1
        channels_active.add(msg.get("channel", "unknown"))

    # Fill in zero-hours for a complete 24-slot heatmap
    full_heatmap = {h: heatmap.get(h, 0) for h in range(24)}

    # Task activity for this agent
    tasks = _load_all_tasks()
    agent_tasks = [
        t for t in tasks if t.get("assignee_session") == session_id
    ]
    tasks_completed = sum(1 for t in agent_tasks if t.get("status") == "completed")
    tasks_failed = sum(1 for t in agent_tasks if t.get("status") == "failed")
    tasks_pending = sum(
        1 for t in agent_tasks if t.get("status") in ("pending", "accepted")
    )

    # Response times (accepted -> completed)
    response_times: list[float] = []
    for t in agent_tasks:
        if t.get("status") == "completed":
            accepted = _parse_iso(t.get("accepted_at", ""))
            completed = _parse_iso(t.get("completed_at", ""))
            if accepted and completed:
                response_times.append((completed - accepted).total_seconds())

    avg_response = (
        round(sum(response_times) / len(response_times), 1)
        if response_times
        else None
    )
    min_response = round(min(response_times), 1) if response_times else None
    max_response = round(max(response_times), 1) if response_times else None

    return {
        "session_id": session_id,
        "window_hours": hours,
        "messages_sent": len(agent_messages),
        "channels_active": sorted(channels_active),
        "heatmap_by_hour": full_heatmap,
        "tasks": {
            "completed": tasks_completed,
            "failed": tasks_failed,
            "in_progress": tasks_pending,
            "total": len(agent_tasks),
        },
        "response_time": {
            "avg_seconds": avg_response,
            "min_seconds": min_response,
            "max_seconds": max_response,
            "samples": len(response_times),
        },
    }


def get_system_health() -> dict[str, Any]:
    """Overall system health dashboard.

    Reports: active agents, message rate, task backlog, channel count,
    and recent error indicators.
    """
    sessions = _load_all_sessions()
    now = _now_utc()

    # Active = heartbeat within last 10 minutes
    active_sessions: list[dict] = []
    for s in sessions:
        hb = _parse_iso(s.get("heartbeat", ""))
        if hb and (now - hb).total_seconds() < 600:
            active_sessions.append(s)

    # Message stats for last hour and last 24h
    stats_1h = get_message_stats(hours=1)
    stats_24h = get_message_stats(hours=24)

    # Task backlog
    tasks = _load_all_tasks()
    pending_tasks = [t for t in tasks if t.get("status") == "pending"]
    accepted_tasks = [t for t in tasks if t.get("status") == "accepted"]
    completed_tasks = [t for t in tasks if t.get("status") == "completed"]
    failed_tasks = [t for t in tasks if t.get("status") == "failed"]

    # Channel count (excluding system channels)
    channel_count = 0
    if CHANNELS_DIR.exists():
        channel_count = sum(
            1 for p in CHANNELS_DIR.glob("*.jsonl")
            if p.stem not in _SYSTEM_CHANNELS
        )

    # Trend: compare first half vs second half of 24h window
    hourly = stats_24h.get("per_hour", {})
    sorted_hours = sorted(hourly.items())
    mid = len(sorted_hours) // 2
    first_half_total = sum(v for _, v in sorted_hours[:mid]) if mid else 0
    second_half_total = sum(v for _, v in sorted_hours[mid:]) if mid else 0
    if first_half_total > 0:
        trend_pct = round((second_half_total - first_half_total) / first_half_total * 100, 1)
    else:
        trend_pct = 0.0 if second_half_total == 0 else 100.0

    if trend_pct > 10:
        trend_label = "increasing"
    elif trend_pct < -10:
        trend_label = "decreasing"
    else:
        trend_label = "stable"

    # Failure trend: compare recent failures to overall rate
    total_finished = len(completed_tasks) + len(failed_tasks)
    failure_rate = (
        round(len(failed_tasks) / total_finished * 100, 1) if total_finished else 0.0
    )

    return {
        "timestamp": now.isoformat(),
        "active_agents": len(active_sessions),
        "active_sessions": [
            {
                "session_id": s.get("session_id"),
                "name": s.get("name"),
                "platform": s.get("platform"),
                "uptime_seconds": round(
                    (now - _parse_iso(s.get("started", now.isoformat()))).total_seconds()  # type: ignore[union-attr]
                )
                if _parse_iso(s.get("started", ""))
                else None,
            }
            for s in active_sessions
        ],
        "channels": channel_count,
        "messages": {
            "last_1h": stats_1h["total_messages"],
            "last_24h": stats_24h["total_messages"],
            "rate_per_hour": stats_1h["messages_per_hour"],
        },
        "tasks": {
            "pending": len(pending_tasks),
            "in_progress": len(accepted_tasks),
            "completed": len(completed_tasks),
            "failed": len(failed_tasks),
            "total": len(tasks),
            "failure_rate": failure_rate,
        },
        "trends": {
            "throughput": trend_label,
            "throughput_change_pct": trend_pct,
            "failure_rate_pct": failure_rate,
        },
    }


def take_snapshot() -> dict[str, Any]:
    """Capture a point-in-time snapshot of all metrics and persist it.

    Snapshots are saved to state/broker/analytics/snapshots/{date}.json
    and an event is logged to events.jsonl.  Returns the snapshot dict.
    """
    now = _now_utc()
    date_str = now.strftime("%Y-%m-%d")

    message_stats = get_message_stats(hours=24)
    task_stats = get_task_stats()
    health = get_system_health()

    snapshot = {
        "date": date_str,
        "captured_at": now.isoformat(),
        "messages": message_stats,
        "tasks": task_stats,
        "health": health,
    }

    snapshot_path = SNAPSHOTS_DIR / f"{date_str}.json"
    _write_json_sync(snapshot_path, snapshot)

    _log_event("snapshot_taken", {"date": date_str, "path": str(snapshot_path)})

    return snapshot
