# 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

"""
Uptime Kuma Service Monitoring Skill

Query monitor status, heartbeat history, and manage monitors via Uptime Kuma's REST API.

Setup:
    UPTIME_KUMA_URL=http://localhost:3001    # Uptime Kuma base URL
    UPTIME_KUMA_API_KEY=your-api-key         # API key from Settings > API Keys
"""

import logging
import os
from datetime import datetime, timezone

import httpx

logger = logging.getLogger(__name__)

REQUEST_TIMEOUT = 15

# Status code mapping used by Uptime Kuma
_STATUS_MAP = {0: "down", 1: "up", 2: "pending", 3: "maintenance"}
_STATUS_REVERSE = {"down": 0, "up": 1, "pending": 2, "maintenance": 3}


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


def _get_config() -> dict:
    return {
        "url": os.environ.get("UPTIME_KUMA_URL", "http://localhost:3001").rstrip("/"),
        "api_key": os.environ.get("UPTIME_KUMA_API_KEY", ""),
    }


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


def _api_headers() -> dict:
    cfg = _get_config()
    headers = {"Accept": "application/json", "Content-Type": "application/json"}
    if cfg["api_key"]:
        headers["Authorization"] = f"Bearer {cfg['api_key']}"
    return headers


def _api_get(path: str) -> dict:
    cfg = _get_config()
    url = f"{cfg['url']}{path}"
    with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
        resp = client.get(url, headers=_api_headers())
        resp.raise_for_status()
        return resp.json()


def _api_post(path: str, body: dict) -> dict:
    cfg = _get_config()
    url = f"{cfg['url']}{path}"
    with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
        resp = client.post(url, json=body, headers=_api_headers())
        resp.raise_for_status()
        return resp.json()


def _api_patch(path: str, body: dict) -> dict:
    cfg = _get_config()
    url = f"{cfg['url']}{path}"
    with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
        resp = client.patch(url, json=body, headers=_api_headers())
        resp.raise_for_status()
        return resp.json()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _epoch_to_iso(ts) -> str:
    if not ts:
        return ""
    try:
        return datetime.fromtimestamp(float(ts), tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    except (ValueError, TypeError, OSError):
        return str(ts)


def _parse_heartbeat(hb: dict) -> dict:
    """Normalise a heartbeat object into a flat dict."""
    status_code = hb.get("status", 2)
    return {
        "status": _STATUS_MAP.get(status_code, "unknown"),
        "status_code": status_code,
        "time": _epoch_to_iso(hb.get("time")),
        "ping_ms": hb.get("ping"),
        "msg": hb.get("msg", ""),
        "important": hb.get("important", False),
    }


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


def get_monitor_status() -> dict:
    """
    Get current status of all Uptime Kuma monitors.

    Returns a summary dict with monitors (list), up_count, down_count, pending_count,
    maintenance_count, total. Each monitor includes id, name, url, status, uptime_24h,
    uptime_30d, avg_response_ms, last_check.

    Requires UPTIME_KUMA_API_KEY for full monitor data.
    Falls back to the public heartbeat endpoint if no API key is set.
    """
    if not _uptime_kuma_available():
        return {
            "monitors": [],
            "up_count": 0,
            "down_count": 0,
            "total": 0,
            "format": "degraded",
            "message": "Uptime Kuma not configured (UPTIME_KUMA_URL not set)",
        }

    cfg = _get_config()

    # Try authenticated API first (requires API key)
    if cfg["api_key"]:
        try:
            data = _api_get("/api/v1/monitor")
            raw_monitors = data if isinstance(data, list) else data.get("monitors", [])

            monitors = []
            for m in raw_monitors:
                status_code = m.get("heartbeat", {}).get("status", 2) if m.get("heartbeat") else 2
                monitors.append(
                    {
                        "id": m.get("id"),
                        "name": m.get("name", ""),
                        "url": m.get("url", ""),
                        "type": m.get("type", "http"),
                        "status": _STATUS_MAP.get(status_code, "unknown"),
                        "active": m.get("active", True),
                        "avg_response_ms": m.get("heartbeat", {}).get("ping")
                        if m.get("heartbeat")
                        else None,
                        "last_check": _epoch_to_iso(m.get("heartbeat", {}).get("time"))
                        if m.get("heartbeat")
                        else "",
                        "uptime_24h": m.get("uptime", {}).get("24", None)
                        if m.get("uptime")
                        else None,
                        "uptime_30d": m.get("uptime", {}).get("720", None)
                        if m.get("uptime")
                        else None,
                        "interval": m.get("interval", 60),
                    }
                )

            up = sum(1 for m in monitors if m["status"] == "up")
            down = sum(1 for m in monitors if m["status"] == "down")
            pending = sum(1 for m in monitors if m["status"] == "pending")
            maintenance = sum(1 for m in monitors if m["status"] == "maintenance")

            return {
                "monitors": monitors,
                "up_count": up,
                "down_count": down,
                "pending_count": pending,
                "maintenance_count": maintenance,
                "total": len(monitors),
            }
        except Exception as e:
            logger.warning("Uptime Kuma authenticated API failed: %s", e)

    # Fallback: public status-page heartbeat (no auth needed)
    try:
        data = _api_get("/api/status-page/heartbeat/all")
        # data is a dict: heartbeatList[monitor_id] = [heartbeats], uptimeList[monitor_id_24] = float
        heartbeat_list = data.get("heartbeatList", {})
        uptime_list = data.get("uptimeList", {})

        monitors = []
        for monitor_id, heartbeats in heartbeat_list.items():
            if not heartbeats:
                continue
            latest = heartbeats[-1] if heartbeats else {}
            status_code = latest.get("status", 2)
            uptime_24h = uptime_list.get(f"{monitor_id}_24", None)
            uptime_30d = uptime_list.get(f"{monitor_id}_720", None)
            monitors.append(
                {
                    "id": monitor_id,
                    "name": latest.get("monitorName", f"Monitor {monitor_id}"),
                    "url": "",
                    "type": "http",
                    "status": _STATUS_MAP.get(status_code, "unknown"),
                    "active": True,
                    "avg_response_ms": latest.get("ping"),
                    "last_check": _epoch_to_iso(latest.get("time")),
                    "uptime_24h": round(float(uptime_24h) * 100, 2)
                    if uptime_24h is not None
                    else None,
                    "uptime_30d": round(float(uptime_30d) * 100, 2)
                    if uptime_30d is not None
                    else None,
                    "interval": 60,
                }
            )

        up = sum(1 for m in monitors if m["status"] == "up")
        down = sum(1 for m in monitors if m["status"] == "down")
        pending = sum(1 for m in monitors if m["status"] == "pending")
        maintenance = sum(1 for m in monitors if m["status"] == "maintenance")

        return {
            "monitors": monitors,
            "up_count": up,
            "down_count": down,
            "pending_count": pending,
            "maintenance_count": maintenance,
            "total": len(monitors),
            "note": "Read from public status page. Set UPTIME_KUMA_API_KEY for full data.",
        }
    except Exception as e:
        logger.warning("Uptime Kuma status page fallback failed: %s", e)
        return {
            "monitors": [],
            "up_count": 0,
            "down_count": 0,
            "total": 0,
            "format": "degraded",
            "message": f"Failed to query Uptime Kuma: {e}",
        }


def get_heartbeat_history(monitor_id: int, limit: int = 50) -> dict:
    """
    Get recent heartbeat history for a specific monitor.

    Args:
        monitor_id: The numeric monitor ID from get_monitor_status().
        limit: Max heartbeats to return (newest first). Default 50.

    Returns:
        Dict with monitor_id, heartbeats (list), up_count, down_count,
        avg_ping_ms, availability_pct.
    """
    if not _uptime_kuma_available():
        return {"format": "degraded", "message": "Uptime Kuma not configured"}

    cfg = _get_config()

    if not cfg["api_key"]:
        return {
            "format": "degraded",
            "message": "UPTIME_KUMA_API_KEY required for heartbeat history",
        }

    try:
        data = _api_get(f"/api/v1/monitor/{monitor_id}/beats?limit={limit}")
        raw_beats = data if isinstance(data, list) else data.get("data", [])

        heartbeats = [_parse_heartbeat(hb) for hb in raw_beats]

        up_count = sum(1 for hb in heartbeats if hb["status"] == "up")
        down_count = sum(1 for hb in heartbeats if hb["status"] == "down")
        pings = [hb["ping_ms"] for hb in heartbeats if hb.get("ping_ms") is not None]
        avg_ping = round(sum(pings) / len(pings), 1) if pings else None
        total = len(heartbeats)
        availability = round(up_count / total * 100, 2) if total else 0.0

        return {
            "monitor_id": monitor_id,
            "heartbeats": heartbeats,
            "up_count": up_count,
            "down_count": down_count,
            "total": total,
            "avg_ping_ms": avg_ping,
            "availability_pct": availability,
        }
    except Exception as e:
        logger.warning("Uptime Kuma heartbeat history failed for %s: %s", monitor_id, e)
        return {"format": "degraded", "message": f"Failed to get heartbeat history: {e}"}


def add_monitor(
    name: str,
    url: str,
    monitor_type: str = "http",
    interval: int = 60,
    retries: int = 1,
) -> dict:
    """
    Add a new monitor to Uptime Kuma.

    Args:
        name: Display name for the monitor.
        url: URL or host to monitor.
        monitor_type: Type of monitor. One of: 'http', 'https', 'tcp', 'ping',
                      'dns', 'keyword'. Default 'http'.
        interval: Check interval in seconds. Default 60.
        retries: Number of retries before marking down. Default 1.

    Returns:
        Dict with the created monitor's id and name on success.
    """
    if not _uptime_kuma_available():
        return {"format": "degraded", "message": "Uptime Kuma not configured"}

    cfg = _get_config()
    if not cfg["api_key"]:
        return {
            "format": "degraded",
            "message": "UPTIME_KUMA_API_KEY required to add monitors",
        }

    _type_map = {
        "http": 1,
        "https": 1,
        "tcp": 4,
        "ping": 7,
        "dns": 10,
        "keyword": 2,
    }
    type_id = _type_map.get(monitor_type.lower(), 1)

    body = {
        "type": type_id,
        "name": name,
        "url": url,
        "interval": interval,
        "retryInterval": interval,
        "maxretries": retries,
        "active": True,
    }

    try:
        result = _api_post("/api/v1/monitor", body)
        return {
            "success": True,
            "monitor_id": result.get("monitorID"),
            "name": name,
            "url": url,
            "type": monitor_type,
            "interval": interval,
        }
    except Exception as e:
        logger.warning("Uptime Kuma add monitor failed: %s", e)
        return {"success": False, "error": str(e)}


def pause_monitor(monitor_id: int) -> dict:
    """
    Pause a monitor (stop checks without deleting it).

    Args:
        monitor_id: The numeric monitor ID.

    Returns:
        Dict with success status.
    """
    if not _uptime_kuma_available():
        return {"format": "degraded", "message": "Uptime Kuma not configured"}

    cfg = _get_config()
    if not cfg["api_key"]:
        return {"format": "degraded", "message": "UPTIME_KUMA_API_KEY required"}

    try:
        _api_patch(f"/api/v1/monitor/{monitor_id}", {"active": False})
        return {"success": True, "monitor_id": monitor_id, "action": "paused"}
    except Exception as e:
        logger.warning("Uptime Kuma pause monitor %s failed: %s", monitor_id, e)
        return {"success": False, "monitor_id": monitor_id, "error": str(e)}


def resume_monitor(monitor_id: int) -> dict:
    """
    Resume a paused monitor (re-enable checks).

    Args:
        monitor_id: The numeric monitor ID.

    Returns:
        Dict with success status.
    """
    if not _uptime_kuma_available():
        return {"format": "degraded", "message": "Uptime Kuma not configured"}

    cfg = _get_config()
    if not cfg["api_key"]:
        return {"format": "degraded", "message": "UPTIME_KUMA_API_KEY required"}

    try:
        _api_patch(f"/api/v1/monitor/{monitor_id}", {"active": True})
        return {"success": True, "monitor_id": monitor_id, "action": "resumed"}
    except Exception as e:
        logger.warning("Uptime Kuma resume monitor %s failed: %s", monitor_id, e)
        return {"success": False, "monitor_id": monitor_id, "error": str(e)}
