#!/usr/bin/env python3
"""
Servidor proxy local para el Reporte de Servidores PROD.
Sirve archivos estaticos Y hace de puente hacia la API de Grafana.
Puerto: 7892
"""

import http.server
import json
import urllib.request
import urllib.error
import os
import ssl
import time

GRAFANA_BASE   = "https://dashboard.monitoring.timlabtesting.com"
DATASOURCE_UID = "e2121950-c313-43e3-b480-55f82ffeadbe"
PORT = 7892

# Token de Grafana — guardado solo en el servidor, nunca llega al navegador
GRAFANA_TOKEN  = "glsa_oEVveT2XPCYqbafV5i8TnCtf7n61vAR9_d890714e"

# ─── Clientes y servidores ────────────────────────────────────────────────────
# os: "linux" usa node_exporter, "windows" usa windows_exporter
CLIENTS = [
    {
        "name": "Cerrejon",
        "color": "#f97316",
        "servers": [
            {"type": "App Server", "instance": "app_cerrejon_prod",  "os": "linux"},
            {"type": "Geo Server", "instance": "cerrejon_prod",      "os": "windows"},
            {"type": "DB Server",  "instance": "db_cerrejon_prod",   "os": "windows"},
        ]
    },
    {
        "name": "MLP",
        "color": "#3b82f6",
        "servers": [
            {"type": "App Server", "instance": "app_mlp_prod",       "os": "linux"},
            {"type": "Geo Server", "instance": "MLP_PRD",            "os": "windows"},
            {"type": "DB Server",  "instance": "db_mlp_prod&qa",     "os": "windows"},
        ]
    },
    {
        "name": "MRN",
        "color": "#a855f7",
        "servers": [
            {"type": "App Server", "instance": "MRN_PRD_APP",        "os": "linux"},
            {"type": "Geo Server", "instance": "MRN_PRD_GEO",        "os": "windows"},
            # Sin DB Server
        ]
    },
    {
        "name": "RAY",
        "color": "#22c55e",
        "servers": [
            {"type": "App Server", "instance": "ray_prod_app_server","os": "linux"},
            {"type": "Geo Server", "instance": "ray_PROD",           "os": "windows"},
            {"type": "DB Server",  "instance": "ray_db_prodqa",      "os": "windows"},
        ]
    },
    {
        "name": "LB",
        "color": "#ec4899",
        "servers": [
            {"type": "App Server", "instance": "app_lb_prod",        "os": "linux"},
            {"type": "Geo Server", "instance": "geo_lb_prod",        "os": "windows"},
            {"type": "DB Server",  "instance": "db_lb_prod",         "os": "windows"},
        ]
    },
]

# ─── Queries Prometheus ───────────────────────────────────────────────────────
def disk_query(instance, os_type):
    if os_type == "windows":
        return (f'(windows_logical_disk_size_bytes{{instance="{instance}",volume=~"^[A-Z]:$"}}'
                f' - windows_logical_disk_free_bytes{{instance="{instance}",volume=~"^[A-Z]:$"}})'
                f' / windows_logical_disk_size_bytes{{instance="{instance}",volume=~"^[A-Z]:$"}}')
    else:
        return (f'(node_filesystem_size_bytes{{instance="{instance}",fstype!~"tmpfs|overlay|squashfs|devtmpfs"}}'
                f' - node_filesystem_avail_bytes{{instance="{instance}",fstype!~"tmpfs|overlay|squashfs|devtmpfs"}})'
                f' / node_filesystem_size_bytes{{instance="{instance}",fstype!~"tmpfs|overlay|squashfs|devtmpfs"}}')

def ram_query(instance, os_type):
    if os_type == "windows":
        return (f'(windows_cs_physical_memory_bytes{{instance="{instance}"}}'
                f' - windows_os_physical_memory_free_bytes{{instance="{instance}"}})'
                f' / windows_cs_physical_memory_bytes{{instance="{instance}"}}')
    else:
        return (f'(node_memory_MemTotal_bytes{{instance="{instance}"}}'
                f' - node_memory_MemFree_bytes{{instance="{instance}"}})'
                f' / node_memory_MemTotal_bytes{{instance="{instance}"}}')

def cpu_query(instance, os_type):
    if os_type == "windows":
        return (f'sum by(instance)(rate(windows_cpu_time_total{{instance="{instance}",mode!="idle"}}[5m]))'
                f' / on(instance) group_left'
                f' sum by(instance)(rate(windows_cpu_time_total{{instance="{instance}"}}[5m]))')
    else:
        return (f'sum by(instance)(rate(node_cpu_seconds_total{{instance="{instance}",mode!="idle"}}[5m])'
                f' / on(instance) group_left'
                f' sum by(instance)(rate(node_cpu_seconds_total{{instance="{instance}"}}[5m])))')

# ─── Handler ──────────────────────────────────────────────────────────────────
class ReporteHandler(http.server.SimpleHTTPRequestHandler):

    def log_message(self, format, *args):
        print(f"  [{self.command}] {self.path} -> {args[0]}")

    def do_OPTIONS(self):
        self.send_response(200)
        self._cors_headers()
        self.end_headers()

    def do_GET(self):
        if self.path.startswith("/api/refresh"):
            self.handle_refresh()
        elif self.path.startswith("/api/history"):
            self.handle_history()
        else:
            super().do_GET()

    def handle_history(self):
        from urllib.parse import urlparse, parse_qs
        parsed  = urlparse(self.path)
        params  = parse_qs(parsed.query)
        rng     = params.get("range", ["24h"])[0]
        client_name = params.get("client", [CLIENTS[0]["name"]])[0]

        range_seconds = {"6h": 6*3600, "24h": 24*3600, "7d": 7*24*3600, "30d": 30*24*3600}
        range_steps   = {"6h": 72,     "24h": 300,      "7d": 1800,      "30d": 7200}
        seconds = range_seconds.get(rng, 24*3600)
        step    = range_steps.get(rng, 300)

        now   = int(time.time())
        start = now - seconds

        # Encontrar el cliente solicitado
        client = next((c for c in CLIENTS if c["name"] == client_name), CLIENTS[0])

        servers_data = []
        for srv in client["servers"]:
            inst, os_t = srv["instance"], srv["os"]

            ts, ram_vals  = self._query_range_single(ram_query(inst, os_t), start, now, step)
            _,  cpu_vals  = self._query_range_single(cpu_query(inst, os_t), start, now, step)
            disk_series   = self._query_range_multi(disk_query(inst, os_t), start, now, step, os_t)

            servers_data.append({
                "type":       srv["type"],
                "os":         os_t,
                "timestamps": ts,
                "ram":        ram_vals,
                "cpu":        cpu_vals,
                "disks":      disk_series,
            })

        self.send_response(200)
        self._cors_headers()
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps({
            "client":  client["name"],
            "color":   client["color"],
            "servers": servers_data,
        }).encode())

    def _query_range_single(self, expr, start, end, step):
        """Retorna (timestamps, values) para la primera serie del resultado."""
        data = self._prom_range(expr, start, end, step)
        if data:
            vals = data[0].get("values", [])
            ts   = [v[0] * 1000 for v in vals]
            vs   = [None if v[1] == "NaN" else round(float(v[1]) * 100, 2) for v in vals]
            return ts, vs
        return [], []

    def _query_range_multi(self, expr, start, end, step, os_type):
        """Retorna lista de {label, values} para queries multi-serie (discos)."""
        import math
        series = []
        for r in self._prom_range(expr, start, end, step):
            lbs   = r.get("metric", {})
            label = lbs.get("volume") if os_type == "windows" else (lbs.get("mountpoint") or lbs.get("device", "?"))
            vals  = r.get("values", [])
            vs    = [None if v[1] == "NaN" else round(float(v[1]) * 100, 2) for v in vals]
            if any(v is not None for v in vs):
                series.append({"label": label or "?", "values": vs})
        return series

    def _prom_range(self, expr, start, end, step):
        import urllib.parse as up
        params = up.urlencode({"query": expr, "start": start, "end": end, "step": step})
        url = f"{GRAFANA_BASE}/api/datasources/proxy/uid/{DATASOURCE_UID}/api/v1/query_range?{params}"
        headers = {"Authorization": f"Bearer {GRAFANA_TOKEN}"} if GRAFANA_TOKEN else {}
        ctx = ssl.create_default_context()
        req = urllib.request.Request(url, headers=headers)
        try:
            with urllib.request.urlopen(req, context=ctx, timeout=20) as resp:
                return json.loads(resp.read()).get("data", {}).get("result", [])
        except Exception as e:
            print(f"  [HISTORY] {e}")
        return []

    def handle_refresh(self):
        # Usa el token del servidor — ignora cualquier header del cliente
        token = GRAFANA_TOKEN

        now_ms  = int(time.time() * 1000)
        from_ms = now_ms - 5 * 60 * 1000

        # Construir todas las queries
        queries = []
        ref_map = []  # (client_idx, server_idx, metric)

        for ci, client in enumerate(CLIENTS):
            for si, server in enumerate(client["servers"]):
                inst = server["instance"]
                os_t = server["os"]

                ref_ram = f"RAM_{ci}_{si}"
                ref_cpu = f"CPU_{ci}_{si}"

                ref_disk = f"DISK_{ci}_{si}"

                queries.append({
                    "refId":      ref_ram,
                    "datasource": {"type": "prometheus", "uid": DATASOURCE_UID},
                    "expr":       ram_query(inst, os_t),
                    "instant":    True, "range": False,
                    "editorMode": "code", "intervalMs": 60000, "maxDataPoints": 100,
                })
                queries.append({
                    "refId":      ref_cpu,
                    "datasource": {"type": "prometheus", "uid": DATASOURCE_UID},
                    "expr":       cpu_query(inst, os_t),
                    "instant":    True, "range": False,
                    "editorMode": "code", "intervalMs": 60000, "maxDataPoints": 100,
                })
                queries.append({
                    "refId":      ref_disk,
                    "datasource": {"type": "prometheus", "uid": DATASOURCE_UID},
                    "expr":       disk_query(inst, os_t),
                    "instant":    True, "range": False,
                    "editorMode": "code", "intervalMs": 60000, "maxDataPoints": 100,
                })

        payload = json.dumps({
            "queries": queries,
            "from":    str(from_ms),
            "to":      str(now_ms),
        }).encode()

        headers = {"Content-Type": "application/json"}
        if token:
            headers["Authorization"] = f"Bearer {token}"

        ctx = ssl.create_default_context()
        req = urllib.request.Request(
            f"{GRAFANA_BASE}/api/ds/query",
            data=payload,
            headers=headers,
            method="POST",
        )

        try:
            with urllib.request.urlopen(req, context=ctx, timeout=15) as resp:
                raw = json.loads(resp.read())
        except urllib.error.HTTPError as e:
            code = e.code
            body = e.read().decode()
            if code == 401:
                self._json_error(401, "No autorizado. Ingresa un API Token valido de Grafana.")
            else:
                self._json_error(code, f"Error Grafana {code}: {body[:300]}")
            return
        except Exception as e:
            self._json_error(500, f"Error de conexion: {str(e)}")
            return

        results_map = raw.get("results", {})

        # Armar respuesta por cliente
        output = []
        for ci, client in enumerate(CLIENTS):
            client_data = {
                "name":    client["name"],
                "color":   client["color"],
                "servers": []
            }
            for si, server in enumerate(client["servers"]):
                ram_val  = self._extract_value(results_map, f"RAM_{ci}_{si}")
                cpu_val  = self._extract_value(results_map, f"CPU_{ci}_{si}")
                disk_val = self._extract_disks(results_map, f"DISK_{ci}_{si}", server["os"])
                client_data["servers"].append({
                    "type":  server["type"],
                    "os":    server["os"],
                    "ram":   round(ram_val * 100, 1) if ram_val is not None else None,
                    "cpu":   round(cpu_val * 100, 1) if cpu_val is not None else None,
                    "disks": disk_val,
                })
            output.append(client_data)

        self.send_response(200)
        self._cors_headers()
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(output).encode())

    def _extract_disks(self, results, ref_id, os_type):
        import math
        disks = []
        try:
            for frame in results[ref_id]["frames"]:
                try:
                    fields = frame.get("schema", {}).get("fields", [])
                    label = None
                    for field in fields:
                        if field.get("name") == "Value":
                            lbs = field.get("labels", {})
                            label = lbs.get("volume") if os_type == "windows" else (lbs.get("mountpoint") or lbs.get("device"))
                            break
                    vals = frame["data"]["values"]
                    if len(vals) >= 2 and vals[1]:
                        v = vals[1][-1]
                        if v is not None and not (isinstance(v, float) and math.isnan(v)):
                            disks.append({"label": label or "?", "pct": round(v * 100, 1)})
                except (KeyError, IndexError, TypeError):
                    continue
        except (KeyError, TypeError):
            pass
        return sorted(disks, key=lambda x: x["pct"], reverse=True)

    def _extract_value(self, results, ref_id):
        try:
            frames = results[ref_id]["frames"]
            if frames:
                values = frames[0]["data"]["values"]
                if len(values) >= 2 and values[1]:
                    val = values[1][-1]
                    # NaN o None = sin datos validos (exporter caido o gap)
                    if val is None:
                        return None
                    import math
                    if isinstance(val, float) and math.isnan(val):
                        return None
                    return val
        except (KeyError, IndexError, TypeError):
            pass
        return None

    def _cors_headers(self):
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Headers", "X-Grafana-Token, Content-Type")

    def _json_error(self, code, msg):
        body = json.dumps({"error": msg}).encode()
        self.send_response(code)
        self._cors_headers()
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(body)


if __name__ == "__main__":
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
    server = http.server.HTTPServer(("", PORT), ReporteHandler)
    print(f"\n[OK] Servidor iniciado en http://localhost:{PORT}")
    print(f"     Abre: http://localhost:{PORT}/reporte_servidores.html\n")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\n[STOP] Servidor detenido.")
