#!/usr/bin/python3
"""MHMT ağ ve sistem paneli ölçüm yardımcısı.

Her alt komut stdout'a tek satır JSON basar; Plasma widget'ları Plasma5Support
"executable" motoruyla çağırır. Hata da JSON olarak döner, çıkış kodu hep 0'dır.
"""
import ipaddress
import json
import os
import socket
import subprocess
import sys
import time

PING_HOST = "1.1.1.1"
DNS_PRESETS = {
    "Cloudflare": ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"],
    "Google": ["8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844"],
}
SPEED_URL = "https://speed.cloudflare.com"
MONITORS = ["plasma-systemmonitor", "ksysguard"]
TOP_N = 5
# ping/iw sayıları noktalı ondalıkla, bağlantı adları UTF-8 basılsın.
ENV = dict(os.environ, LC_ALL="C.UTF-8")


def out(obj):
    print(json.dumps(obj, ensure_ascii=False))


def run(argv, timeout=10):
    try:
        p = subprocess.run(argv, capture_output=True, text=True, env=ENV, timeout=timeout)
        return p.returncode, p.stdout, p.stderr
    except (OSError, subprocess.TimeoutExpired) as e:
        return 1, "", str(e)


def read(path):
    try:
        with open(path, encoding="utf-8", errors="replace") as f:
            return f.read().strip()
    except OSError:
        return None


def read_int(path):
    try:
        return int(read(path))
    except (TypeError, ValueError):
        return None


def nm_list(s):
    # nmcli -g ':' karakterini kaçırır, çok değeri ' | ' ile birleştirir.
    return [v.replace("\\:", ":").strip() for v in (s or "").split(" | ") if v.strip()]


def default_route():
    _, o, _ = run(["ip", "-j", "-4", "route", "show", "default"])
    try:
        routes = json.loads(o or "[]")
    except ValueError:
        return None
    routes = [r for r in routes if r.get("dev")]
    if not routes:
        return None
    return min(routes, key=lambda r: r.get("metric", 0))


def connection(dev):
    _, o, _ = run(["nmcli", "-g", "GENERAL.TYPE,GENERAL.CONNECTION,GENERAL.CON-UUID,IP4.DNS,IP6.DNS",
                   "device", "show", dev])
    lines = (o.split("\n") + [""] * 5)[:5]
    typ, con, uuid, dns4, dns6 = lines
    dns = {"mode": "DHCP", "servers": [], "v6method": "auto"}
    if uuid:
        _, c, _ = run(["nmcli", "-g", "ipv4.dns,ipv4.ignore-auto-dns,ipv6.method", "connection", "show", uuid])
        servers, ignore, v6method = (c.split("\n") + ["", "no", "auto"])[:3]
        servers = [s.strip() for s in servers.split(",") if s.strip()]
        if not servers and ignore != "yes":
            mode = "DHCP"
        elif "1.1.1.1" in servers:
            mode = "Cloudflare"
        elif "8.8.8.8" in servers:
            mode = "Google"
        else:
            mode = "Custom"
        dns = {"mode": mode, "servers": servers, "v6method": v6method or "auto"}
    return {"type": typ, "con": con, "uuid": uuid, "dns": dns, "active": nm_list(dns4) + nm_list(dns6)}


def cmd_net(_args):
    rt = default_route()
    if not rt:
        return out({"online": False})
    dev = rt["dev"]
    _, o, _ = run(["ip", "-j", "-4", "addr", "show", "dev", dev])
    ip = ""
    try:
        for iface in json.loads(o or "[]"):
            for a in iface.get("addr_info", []):
                ip = ip or a.get("local", "")
    except ValueError:
        pass
    info = {"online": True, "dev": dev, "gw": rt.get("gateway", ""), "ip": ip}
    info.update(connection(dev))
    link = 0.0
    if info["type"] == "wifi":
        _, o, _ = run(["iw", "dev", dev, "link"])
        for line in o.splitlines():
            if "tx bitrate:" in line:
                try:
                    link = float(line.split("tx bitrate:")[1].split()[0])
                except (IndexError, ValueError):
                    pass
    else:
        link = float(read_int(f"/sys/class/net/{dev}/speed") or 0)
    info["link"] = max(link, 0.0)
    out(info)


def cmd_rate(args):
    dev = os.path.basename(args[0]) if args else ""
    base = f"/sys/class/net/{dev}/statistics"
    rx, tx = read_int(f"{base}/rx_bytes"), read_int(f"{base}/tx_bytes")
    if rx is None or tx is None:
        return out(None)
    out({"dev": dev, "rx": rx, "tx": tx, "t": time.monotonic()})


def cmd_ping(_args):
    _, o, _ = run(["ping", "-c", "3", "-i", "0.3", "-q", "-W", "2", PING_HOST], timeout=10)
    loss = avg = None
    for line in o.splitlines():
        if "packet loss" in line:
            for part in line.split(","):
                if "packet loss" in part:
                    try:
                        loss = float(part.strip().split("%")[0])
                    except ValueError:
                        pass
        if "min/avg" in line and "=" in line:
            try:
                avg = float(line.split("=")[1].strip().split("/")[1])
            except (IndexError, ValueError):
                pass
    out({"ms": avg, "loss": loss})


def cmd_speed_down(_args):
    # Cloudflare 100 MB ve üstü tek isteği reddediyor: ~8 sn boyunca 25 MB'lık parçalar.
    total = secs = 0.0
    for _ in range(40):
        if secs >= 8:
            break
        rc, o, _ = run(["curl", "-s", "-o", "/dev/null", "--max-time", str(max(2, int(10 - secs) + 1)),
                        "-w", "%{http_code} %{size_download} %{time_total}",
                        f"{SPEED_URL}/__down?bytes=25000000"], timeout=15)
        try:
            code, size, t = o.split()
            size, t = float(size), float(t)
        except ValueError:
            break
        if code != "200" or t <= 0:
            break
        total += size
        secs += t
    out({"mbps": round(total * 8 / secs / 1e6) if secs > 0 else None})


def cmd_speed_up(_args):
    rc, o, _ = run(["sh", "-c", "head -c 50000000 /dev/zero | curl -s -o /dev/null --max-time 10 "
                    f"-w '%{{speed_upload}}' --data-binary @- {SPEED_URL}/__up"], timeout=15)
    try:
        bps = float(o)
    except ValueError:
        bps = 0
    out({"mbps": round(bps * 8 / 1e6) if bps > 0 else None})


def cmd_dns(args):
    if not args or args[0] not in ("DHCP", "Cloudflare", "Google", "Custom"):
        return out({"ok": False, "error": "geçersiz DNS kipi"})
    mode = args[0]
    if mode == "DHCP":
        addrs = []
    elif mode == "Custom":
        addrs = []
        for a in " ".join(args[1:]).replace(",", " ").split():
            try:
                addrs.append(str(ipaddress.ip_address(a)))
            except ValueError:
                return out({"ok": False, "error": f"geçersiz adres: {a}"})
        if not addrs:
            return out({"ok": False, "error": "adres girilmedi"})
    else:
        addrs = DNS_PRESETS[mode]
    # Aygıt ve bağlantı widget'tan alınmaz, o anki varsayılan yoldan okunur.
    rt = default_route()
    if not rt:
        return out({"ok": False, "error": "etkin bağlantı yok"})
    dev = rt["dev"]
    conn = connection(dev)
    if not conn["uuid"]:
        return out({"ok": False, "error": "NetworkManager bağlantısı yok"})
    ignore = "no" if mode == "DHCP" else "yes"
    v4 = ",".join(a for a in addrs if ":" not in a)
    v6 = ",".join(a for a in addrs if ":" in a)
    argv = ["nmcli", "connection", "modify", conn["uuid"], "ipv4.dns", v4, "ipv4.ignore-auto-dns", ignore]
    if conn["dns"]["v6method"] not in ("ignore", "disabled"):
        argv += ["ipv6.dns", v6, "ipv6.ignore-auto-dns", ignore]
    # polkit parola sorabilir; kullanıcı yanıtlayana kadar bekle.
    rc, _, e = run(argv, timeout=120)
    if rc != 0:
        return out({"ok": False, "error": e.strip() or "nmcli connection modify"})
    rc, _, e = run(["nmcli", "device", "reapply", dev], timeout=30)
    if rc != 0:
        return out({"ok": False, "error": e.strip() or "nmcli device reapply"})
    out({"ok": True})


def hwmon():
    names, fans = {}, []
    base = "/sys/class/hwmon"
    try:
        entries = sorted(os.listdir(base))
    except OSError:
        entries = []
    for d in entries:
        path = f"{base}/{d}"
        name = read(f"{path}/name")
        if name and name not in names:
            names[name] = path
        try:
            files = sorted(os.listdir(path))
        except OSError:
            files = []
        fans += [f"{path}/{f}" for f in files if f.startswith("fan") and f.endswith("_input")]
    return names, fans


def temp(path):
    v = read_int(f"{path}/temp1_input") if path else None
    return round(v / 1000) if v is not None else None


def os_name():
    for line in (read("/etc/os-release") or "").splitlines():
        if line.startswith("PRETTY_NAME="):
            return line.split("=", 1)[1].strip().strip('"')
    return ""


def battery():
    base = "/sys/class/power_supply"
    try:
        bats = sorted(n for n in os.listdir(base) if n.startswith("BAT"))
    except OSError:
        bats = []
    if not bats:
        return None
    p = f"{base}/{bats[0]}"
    watts = read_int(f"{p}/power_now")
    if watts is not None:
        watts /= 1e6
    else:
        a, v = read_int(f"{p}/current_now"), read_int(f"{p}/voltage_now")
        watts = a * v / 1e12 if a is not None and v is not None else None
    return {"capacity": read_int(f"{p}/capacity"), "status": read(f"{p}/status") or "",
            "watts": round(watts, 1) if watts is not None else None}


def cmd_sys(_args):
    stat = (read("/proc/stat") or "cpu 0").split("\n")[0].split()[1:]
    f = [int(x) for x in stat]
    idle = f[3] + (f[4] if len(f) > 4 else 0)
    mem = {}
    for line in (read("/proc/meminfo") or "").splitlines():
        k, _, v = line.partition(":")
        try:
            mem[k] = int(v.split()[0]) * 1024
        except (IndexError, ValueError):
            pass
    vfs = os.statvfs("/")
    model = ""
    for line in (read("/proc/cpuinfo") or "").splitlines():
        if line.startswith("model name"):
            model = line.split(":", 1)[1].strip().split(" with ")[0]
            break
    names, fans = hwmon()
    cpu_hw = next((names[n] for n in ("k10temp", "zenpower", "coretemp", "acpitz") if n in names), None)
    rpm = [r for r in (read_int(p) for p in fans) if r is not None]
    out({
        "host": socket.gethostname(),
        "model": model,
        "os": os_name(),
        "kernel": read("/proc/sys/kernel/osrelease") or "",
        "threads": os.cpu_count() or 0,
        "uptime": float((read("/proc/uptime") or "0").split()[0]),
        "load": (read("/proc/loadavg") or "").split()[:3],
        "cpu": {"idle": idle, "total": sum(f)},
        "mem": {"total": mem.get("MemTotal", 0), "used": mem.get("MemTotal", 0) - mem.get("MemAvailable", 0)},
        "swap": {"total": mem.get("SwapTotal", 0), "used": mem.get("SwapTotal", 0) - mem.get("SwapFree", 0)},
        "disk": {"total": vfs.f_blocks * vfs.f_frsize, "used": (vfs.f_blocks - vfs.f_bfree) * vfs.f_frsize},
        "temp": {"cpu": temp(cpu_hw), "gpu": temp(names.get("amdgpu")), "ssd": temp(names.get("nvme"))},
        "fans": rpm,
        "battery": battery(),
    })


def proc_times():
    times = {}
    for pid in os.listdir("/proc"):
        if not pid.isdigit():
            continue
        s = read(f"/proc/{pid}/stat")
        if not s:
            continue
        # comm parantez içinde ve boşluk içerebilir; alanlar son ')' sonrasından sayılır.
        head, _, rest = s.rpartition(")")
        fields = rest.split()
        try:
            times[pid] = (head.split("(", 1)[1], int(fields[11]) + int(fields[12]))
        except (IndexError, ValueError):
            pass
    return times


def proc_name(pid, comm):
    # comm 15 karakterde kesilir; komut satırının ilk öğesi tam adı verir.
    argv0 = (read(f"/proc/{pid}/cmdline") or "").split("\x00")[0]
    name = os.path.basename(argv0.split()[0]) if argv0.strip() else ""
    return name if name.startswith(comm[:15]) or not comm else comm


def cmd_top(_args):
    interval = 0.5
    a = proc_times()
    time.sleep(interval)
    b = proc_times()
    hz = os.sysconf("SC_CLK_TCK")
    cores = os.cpu_count() or 1
    me = str(os.getpid())
    rows = []
    for pid, (name, t) in b.items():
        if pid == me or pid not in a:
            continue
        rows.append((t - a[pid][1], name, pid))
    rows = sorted((r for r in rows if r[0] > 0), reverse=True)[:TOP_N]
    out({"procs": [{"name": proc_name(pid, n), "cpu": round(d / hz / interval / cores * 100, 1)}
                   for d, n, pid in rows]})


def cmd_monitor(_args):
    for exe in MONITORS:
        for d in os.environ.get("PATH", "/usr/bin").split(":"):
            if os.access(os.path.join(d, exe), os.X_OK):
                subprocess.Popen([exe], start_new_session=True, stdin=subprocess.DEVNULL,
                                 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                return out({"ok": True})
    out({"ok": False, "error": "sistem izleyici kurulu değil"})


COMMANDS = {"net": cmd_net, "rate": cmd_rate, "ping": cmd_ping, "speed-down": cmd_speed_down,
            "speed-up": cmd_speed_up, "dns": cmd_dns, "sys": cmd_sys, "top": cmd_top, "monitor": cmd_monitor}

if __name__ == "__main__":
    cmd = COMMANDS.get(sys.argv[1] if len(sys.argv) > 1 else "")
    if not cmd:
        out({"error": "kullanım: probe " + "|".join(COMMANDS)})
    else:
        try:
            cmd(sys.argv[2:])
        except Exception as e:  # widget her durumda ayrıştırılabilir çıktı alsın
            out({"error": str(e)})
