# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Netdata Real-Time Metrics Skill

Query host metrics, alarms, and process/interface data from a local Netdata instance.

Setup:
    NETDATA_URL=http://localhost:19999   # Netdata web API base URL
"""

import logging
import os
from typing import Optional

import httpx

logger = logging.getLogger(__name__)

REQUEST_TIMEOUT = 15


# ---------------------------------------------------------------------------
# Configuration helpers
# ---------------------------------------------------------------------------


def _get_config() -> dict:
    return {
        "url": os.environ.get("NETDATA_URL", "http://localhost:19999").rstrip("/"),
    }


def _netdata_available() -> bool:
    cfg = _get_config()
    return bool(cfg["url"])


def _api_get(path: str) -> dict:
    """GET from Netdata API. Returns parsed JSON or raises."""
    cfg = _get_config()
    url = f"{cfg['url']}{path}"
    with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
        resp = client.get(url)
        resp.raise_for_status()
        return resp.json()


# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------


def get_host_metrics(
    after: int = -300,
    points: int = 1,
) -> dict:
    """
    Get current host system metrics (CPU, RAM, disk, network) from Netdata.

    Args:
        after: Seconds relative to now (negative) or Unix timestamp. Default -300 = last 5 min.
        points: Number of data points to return. Default 1 = latest value only.

    Returns:
        Dict with cpu_pct, ram_pct, ram_used_gb, ram_total_gb, disk_read_mbps,
        disk_write_mbps, net_rx_mbps, net_tx_mbps, uptime_seconds, hostname.
    """
    if not _netdata_available():
        return {"format": "degraded", "message": "Netdata not configured (NETDATA_URL not set)"}

    result: dict = {}

    try:
        info = _api_get("/api/v1/info")
        result["hostname"] = info.get("hostname", "localhost")
        result["netdata_version"] = info.get("version", "")
        os_info = info.get("os", {})
        result["os"] = os_info if isinstance(os_info, str) else str(os_info)
        result["uptime_seconds"] = info.get("uptime", 0)
    except Exception as e:
        logger.warning("Netdata info failed: %s", e)
        result["hostname"] = "localhost"
        result["uptime_seconds"] = 0

    def _chart_latest(chart: str, dimension: Optional[str] = None) -> float:
        """Return the most recent value for a chart dimension."""
        try:
            path = f"/api/v1/data?chart={chart}&after={after}&points={points}&format=json"
            data = _api_get(path)
            labels = data.get("labels", [])
            rows = data.get("data", [])
            if not rows:
                return 0.0
            latest = rows[-1]
            if dimension and dimension in labels:
                idx = labels.index(dimension)
                val = latest[idx]
                return float(val) if val is not None else 0.0
            # Return first non-time value
            if len(latest) > 1:
                val = latest[1]
                return float(val) if val is not None else 0.0
        except Exception as e:
            logger.debug("Netdata chart %s failed: %s", chart, e)
        return 0.0

    # CPU — system.cpu: percentage 0-100
    result["cpu_pct"] = round(_chart_latest("system.cpu", "system"), 2)

    # RAM — system.ram: values in MiB
    try:
        path = f"/api/v1/data?chart=system.ram&after={after}&points={points}&format=json"
        ram_data = _api_get(path)
        labels = ram_data.get("labels", [])
        rows = ram_data.get("data", [])
        if rows:
            latest = rows[-1]
            row_dict = {
                lbl: (float(latest[i]) if latest[i] is not None else 0.0)
                for i, lbl in enumerate(labels)
                if i > 0
            }
            used_mib = (
                row_dict.get("used", 0.0)
                + row_dict.get("buffers", 0.0)
                + row_dict.get("cached", 0.0)
            )
            free_mib = row_dict.get("free", 0.0)
            total_mib = used_mib + free_mib
            result["ram_used_gb"] = round(used_mib / 1024, 2)
            result["ram_total_gb"] = round(total_mib / 1024, 2)
            result["ram_pct"] = round((used_mib / total_mib * 100) if total_mib else 0.0, 1)
        else:
            result.update({"ram_used_gb": 0.0, "ram_total_gb": 0.0, "ram_pct": 0.0})
    except Exception as e:
        logger.warning("Netdata RAM chart failed: %s", e)
        result.update({"ram_used_gb": 0.0, "ram_total_gb": 0.0, "ram_pct": 0.0})

    # Disk I/O — system.io in kilobytes/s → convert to MB/s
    result["disk_read_mbps"] = round(_chart_latest("system.io", "in") / 1024, 3)
    result["disk_write_mbps"] = round(abs(_chart_latest("system.io", "out")) / 1024, 3)

    # Disk space — disk_space._ is the root filesystem (values in GiB)
    # Netdata also exposes disk_space.<mountpoint> for other mounts
    try:
        path = f"/api/v1/data?chart=disk_space._&after={after}&points={points}&format=json"
        disk_data = _api_get(path)
        disk_labels = disk_data.get("labels", [])
        disk_rows = disk_data.get("data", [])
        if disk_rows:
            disk_latest = disk_rows[-1]
            disk_dict = {
                lbl: (float(disk_latest[i]) if disk_latest[i] is not None else 0.0)
                for i, lbl in enumerate(disk_labels)
                if i > 0
            }
            used_gb = disk_dict.get("used", 0.0)
            avail_gb = disk_dict.get("avail", 0.0)
            reserved_gb = disk_dict.get("reserved_for_root", 0.0)
            total_gb = used_gb + avail_gb + reserved_gb
            result["disk_used_gb"] = round(used_gb, 2)
            result["disk_total_gb"] = round(total_gb, 2)
            result["disk_pct"] = round((used_gb / total_gb * 100) if total_gb else 0.0, 1)
        else:
            result.update({"disk_used_gb": 0.0, "disk_total_gb": 0.0, "disk_pct": 0.0})
    except Exception as e:
        logger.debug("Netdata disk_space chart failed: %s", e)
        result.update({"disk_used_gb": 0.0, "disk_total_gb": 0.0, "disk_pct": 0.0})

    # Network — system.net in kilobits/s → convert to Mb/s
    result["net_rx_mbps"] = round(_chart_latest("system.net", "received") / 1024, 3)
    result["net_tx_mbps"] = round(abs(_chart_latest("system.net", "sent")) / 1024, 3)

    return result


def get_top_processes(limit: int = 10) -> dict:
    """
    Get top processes by CPU and RAM usage from Netdata.

    Args:
        limit: Maximum number of processes to return per category. Default 10.

    Returns:
        Dict with top_cpu (list) and top_ram (list), each with pid, name, cpu_pct, ram_mb.
    """
    if not _netdata_available():
        return {"format": "degraded", "message": "Netdata not configured (NETDATA_URL not set)"}

    try:
        # Netdata groups processes under apps.* charts
        cpu_data = _api_get("/api/v1/data?chart=apps.cpu&after=-60&points=1&format=json")
        ram_data = _api_get("/api/v1/data?chart=apps.mem&after=-60&points=1&format=json")

        def _extract_rows(data: dict) -> list[dict]:
            labels = data.get("labels", [])
            rows = data.get("data", [])
            if not rows:
                return []
            latest = rows[-1]
            result = []
            for i, lbl in enumerate(labels):
                if i == 0:  # time column
                    continue
                val = latest[i]
                if val is not None and float(val) > 0:
                    result.append({"name": lbl, "value": round(float(val), 2)})
            return sorted(result, key=lambda x: x["value"], reverse=True)[:limit]

        cpu_rows = _extract_rows(cpu_data)
        ram_rows = _extract_rows(ram_data)

        return {
            "top_cpu": [{"name": r["name"], "cpu_pct": r["value"]} for r in cpu_rows],
            "top_ram": [{"name": r["name"], "ram_mb": r["value"]} for r in ram_rows],
            "count": max(len(cpu_rows), len(ram_rows)),
        }
    except Exception as e:
        logger.warning("Netdata top processes failed: %s", e)
        return {"format": "degraded", "message": f"Failed to query Netdata: {e}"}


def get_interface_traffic(interface: str = "eth0") -> dict:
    """
    Get current RX/TX traffic statistics for a network interface.

    Args:
        interface: Interface name (e.g. 'eth0', 'ens3', 'wlan0'). Default 'eth0'.

    Returns:
        Dict with interface, rx_mbps, tx_mbps, rx_packets_s, tx_packets_s,
        rx_errors, tx_errors.
    """
    if not _netdata_available():
        return {"format": "degraded", "message": "Netdata not configured (NETDATA_URL not set)"}

    try:
        # Netdata interface charts: net.<interface>
        chart = f"net.{interface}"
        data = _api_get(f"/api/v1/data?chart={chart}&after=-60&points=1&format=json")
        labels = data.get("labels", [])
        rows = data.get("data", [])

        if not rows:
            return {
                "interface": interface,
                "error": f"No data for interface '{interface}'. Check interface name.",
            }

        latest = rows[-1]
        row_dict = {
            lbl: (float(latest[i]) if latest[i] is not None else 0.0)
            for i, lbl in enumerate(labels)
            if i > 0
        }

        # Values in kilobits/s → convert to Mb/s
        rx_kbps = row_dict.get("received", 0.0)
        tx_kbps = abs(row_dict.get("sent", 0.0))

        # Packet charts: net_packets.<interface>
        try:
            pkt_chart = f"net_packets.{interface}"
            pkt_data = _api_get(f"/api/v1/data?chart={pkt_chart}&after=-60&points=1&format=json")
            pkt_labels = pkt_data.get("labels", [])
            pkt_rows = pkt_data.get("data", [])
            if pkt_rows:
                pkt_latest = pkt_rows[-1]
                pkt_dict = {
                    lbl: (float(pkt_latest[i]) if pkt_latest[i] is not None else 0.0)
                    for i, lbl in enumerate(pkt_labels)
                    if i > 0
                }
                rx_pkts = pkt_dict.get("received", 0.0)
                tx_pkts = abs(pkt_dict.get("sent", 0.0))
            else:
                rx_pkts = tx_pkts = 0.0
        except Exception:
            rx_pkts = tx_pkts = 0.0

        # Error charts: net_errors.<interface>
        try:
            err_chart = f"net_errors.{interface}"
            err_data = _api_get(f"/api/v1/data?chart={err_chart}&after=-60&points=1&format=json")
            err_labels = err_data.get("labels", [])
            err_rows = err_data.get("data", [])
            if err_rows:
                err_latest = err_rows[-1]
                err_dict = {
                    lbl: (float(err_latest[i]) if err_latest[i] is not None else 0.0)
                    for i, lbl in enumerate(err_labels)
                    if i > 0
                }
                rx_errors = err_dict.get("inbound", 0.0)
                tx_errors = err_dict.get("outbound", 0.0)
            else:
                rx_errors = tx_errors = 0.0
        except Exception:
            rx_errors = tx_errors = 0.0

        return {
            "interface": interface,
            "rx_mbps": round(rx_kbps / 1024, 4),
            "tx_mbps": round(tx_kbps / 1024, 4),
            "rx_kbps": round(rx_kbps, 2),
            "tx_kbps": round(tx_kbps, 2),
            "rx_packets_s": round(rx_pkts, 1),
            "tx_packets_s": round(tx_pkts, 1),
            "rx_errors": round(rx_errors, 0),
            "tx_errors": round(tx_errors, 0),
        }
    except Exception as e:
        logger.warning("Netdata interface traffic failed for %s: %s", interface, e)
        return {"format": "degraded", "message": f"Failed to query interface '{interface}': {e}"}


def get_system_alarms() -> dict:
    """
    Get all active Netdata alarms and recent alarm transitions.

    Returns:
        Dict with active (list of alarm dicts), count, critical_count, warning_count.
        Each alarm has: id, name, chart, family, status, value, units, info, last_updated.
    """
    if not _netdata_available():
        return {
            "active": [],
            "count": 0,
            "critical_count": 0,
            "warning_count": 0,
            "format": "degraded",
            "message": "Netdata not configured (NETDATA_URL not set)",
        }

    try:
        data = _api_get("/api/v1/alarms?active")
        alarms_raw = data.get("alarms", {})

        active = []
        critical_count = 0
        warning_count = 0

        for alarm_id, alarm in alarms_raw.items():
            status = alarm.get("status", "").upper()
            if status not in ("WARNING", "CRITICAL"):
                continue

            entry = {
                "id": alarm_id,
                "name": alarm.get("name", ""),
                "chart": alarm.get("chart", ""),
                "family": alarm.get("family", ""),
                "status": status,
                "value": alarm.get("value"),
                "units": alarm.get("units", ""),
                "info": alarm.get("info", ""),
                "last_updated": alarm.get("last_updated", 0),
            }
            active.append(entry)

            if status == "CRITICAL":
                critical_count += 1
            elif status == "WARNING":
                warning_count += 1

        active.sort(key=lambda x: (0 if x["status"] == "CRITICAL" else 1, -x["last_updated"]))

        return {
            "active": active,
            "count": len(active),
            "critical_count": critical_count,
            "warning_count": warning_count,
        }
    except Exception as e:
        logger.warning("Netdata alarms failed: %s", e)
        return {
            "active": [],
            "count": 0,
            "critical_count": 0,
            "warning_count": 0,
            "format": "degraded",
            "message": f"Failed to query Netdata alarms: {e}",
        }
