"""
telemetry.py — Telemetria V6 do Neurotron
-----------------------------------------
Responsável por:
  • Medições básicas (CPU, MEM, LOAD, IO) via /proc
  • Delta entre ciclos
  • Aceleração do delta
  • Temperatura virtual (fadiga cognitiva)
  • Jitter cognitivo (latência entre ciclos)
  • FS Health (blocos, erros, RO-mode)
  • Eventos telemétricos (V6)
  • Classificação de estados cognitivos

Totalmente stdlib + /proc:
  - sem psutil
  - sem glibc
  - compatível com Python estático + musl

O Cortex só chama TelemetryV6.step() a cada ciclo.
"""

import os
import time
from pathlib import Path

from .logbus import logbus


class TelemetryV6:
    # ----------------------------------------------------------
    #  Inicialização
    # ----------------------------------------------------------
    def __init__(self, event_callback=None):
        """
        event_callback(event_name, payload_dict)
        """
        self.event_callback = event_callback

        self.last_raw = None
        self.last_delta = None
        self.last_ts = None

        self.temperature = 0.0
        self.jitter_avg = 0.0

        # manter último estado cognitivo
        self.last_state = "stable"

    # ==========================================================
    #  Sensores básicos (CPU, MEM, LOAD, IO) — via /proc
    # ==========================================================

    # ---------------- CPU ----------------
    def _read_proc_stat(self):
        """Lê a linha 'cpu ' de /proc/stat. Retorna dict ou None."""
        try:
            with open("/proc/stat", "r", encoding="utf-8") as f:
                line = f.readline()
            if not line.startswith("cpu "):
                return None

            parts = line.strip().split()[1:]
            # primeiros 10 campos são estáveis há muitos kernels
            vals = list(map(int, parts[:10]))
            return {
                "user": vals[0],
                "nice": vals[1],
                "system": vals[2],
                "idle": vals[3],
                "iowait": vals[4],
                "irq": vals[5],
                "softirq": vals[6],
                "steal": vals[7],
                "guest": vals[8],
                "guest_nice": vals[9],
            }
        except Exception:
            return None

    def _cpu_percent(self, interval=0.05):
        """Computa CPU% entre duas leituras de /proc/stat."""
        a = self._read_proc_stat()
        if not a:
            return 0.0

        time.sleep(interval)

        b = self._read_proc_stat()
        if not b:
            return 0.0

        idle_a = a["idle"] + a["iowait"]
        idle_b = b["idle"] + b["iowait"]

        non_a = sum(a.values()) - idle_a
        non_b = sum(b.values()) - idle_b

        total_a = idle_a + non_a
        total_b = idle_b + non_b

        totald = total_b - total_a
        idled = idle_b - idle_a

        if totald <= 0:
            return 0.0

        usage = (totald - idled) * 100.0 / totald
        return round(max(0.0, min(usage, 100.0)), 1)

    # ---------------- MEMÓRIA ----------------
    def _mem_percent(self):
        try:
            info = {}
            with open("/proc/meminfo", "r", encoding="utf-8") as f:
                for line in f:
                    k, v = line.split(":", 1)
                    info[k.strip()] = v.strip()

            def kB(key):
                return float(info[key].split()[0]) if key in info else None

            mem_total = kB("MemTotal")
            mem_avail = kB("MemAvailable")

            if mem_total is None or mem_avail is None or mem_total <= 0:
                return 0.0

            used = mem_total - mem_avail
            return round(max(0.0, min(used * 100.0 / mem_total, 100.0)), 1)

        except Exception:
            return 0.0

    # ---------------- LOADAVG ----------------
    def _loadavg(self):
        try:
            if hasattr(os, "getloadavg"):
                l1, l5, l15 = os.getloadavg()
                return [round(l1, 2), round(l5, 2), round(l15, 2)]

            with open("/proc/loadavg", "r", encoding="utf-8") as f:
                parts = f.read().strip().split()
                l1, l5, l15 = map(float, parts[:3])
                return [round(l1, 2), round(l5, 2), round(l15, 2)]

        except Exception:
            return [0.0, 0.0, 0.0]

    # ---------------- DISK IO (bytes cumulativos) ----------------
    def _disk_bytes(self):
        """
        Lê /proc/diskstats e soma bytes lidos+escritos de discos reais.
        Usa 512 bytes por setor como aproximação clássica.
        """
        try:
            total = 0
            if not os.path.exists("/proc/diskstats"):
                return 0.0

            with open("/proc/diskstats", "r", encoding="utf-8") as f:
                for line in f:
                    parts = line.split()
                    if len(parts) < 14:
                        continue

                    name = parts[2]

                    # ignora loop/ram/partições virtuais
                    if name.startswith(("loop", "ram")):
                        continue

                    # campos: ... sectors_read (5) ... sectors_written (9) ...
                    try:
                        sectors_read = int(parts[5])
                        sectors_written = int(parts[9])
                    except Exception:
                        continue

                    total += (sectors_read + sectors_written) * 512

            return float(total)
        except Exception:
            return 0.0

    # ----------------------------------------------------------
    #  Coleta RAW (CPU, MEM, LOAD, IO)
    # ----------------------------------------------------------
    def collect_raw(self):
        now = time.monotonic()

        cpu = self._cpu_percent()
        mem = self._mem_percent()
        load1 = self._loadavg()[0]
        disk_total = self._disk_bytes()

        return {
            "ts": now,
            "cpu": cpu,
            "mem": mem,
            "load": load1,
            "disk": disk_total,
        }

    # ----------------------------------------------------------
    #  Delta = diferença entre ciclos
    # ----------------------------------------------------------
    def compute_delta(self, raw):
        if self.last_raw is None:
            self.last_raw = raw
            return {k: 0.0 for k in raw.keys() if k != "ts"}

        delta = {
            "cpu": raw["cpu"] - self.last_raw["cpu"],
            "mem": raw["mem"] - self.last_raw["mem"],
            "load": raw["load"] - self.last_raw["load"],
            "disk": raw["disk"] - self.last_raw["disk"],
        }

        self.last_delta = delta
        self.last_raw = raw
        return delta

    # ----------------------------------------------------------
    #  Aceleração = variação do delta
    # ----------------------------------------------------------
    def compute_accel(self, delta):
        if self.last_delta is None:
            return {k: 0.0 for k in delta.keys()}

        accel = {k: delta[k] - self.last_delta[k] for k in delta.keys()}
        return accel

    # ----------------------------------------------------------
    #  Jitter Cognitivo
    # ----------------------------------------------------------
    def compute_jitter(self, now):
        if self.last_ts is None:
            self.last_ts = now
            return 0.0

        jitter = now - self.last_ts
        self.last_ts = now

        # média móvel simples
        self.jitter_avg = (self.jitter_avg * 0.9) + (jitter * 0.1)
        return jitter

    # ----------------------------------------------------------
    #  Temperatura Virtual (fadiga cognitiva)
    # ----------------------------------------------------------
    def compute_temperature(self, raw, delta):
        # modelo simplificado (ajustável no futuro)
        score = (
            raw["cpu"] * 0.6 +
            raw["load"] * 0.3 +
            abs(delta["cpu"]) * 0.2 +
            raw["mem"] * 0.1
        )

        # decay + acumulação
        self.temperature = max(0.0, self.temperature * 0.90 + score * 0.10)
        return self.temperature

    # ----------------------------------------------------------
    #  FS Health (sem psutil)
    # ----------------------------------------------------------
    def compute_fs_health(self):
        status = {
            "read_only": False,
            "io_errors": 0,
            "free_percent": None,
        }

        # espaço livre via statvfs
        try:
            st = os.statvfs("/")
            total = st.f_frsize * st.f_blocks
            free = st.f_frsize * st.f_bfree
            if total > 0:
                status["free_percent"] = free * 100.0 / total
        except Exception:
            status["free_percent"] = None

        # leitura de erros EXT4 se existirem
        try:
            base = "/sys/fs/ext4"
            if os.path.isdir(base):
                total_errors = 0
                for name in os.listdir(base):
                    err_path = os.path.join(base, name, "errors_count")
                    if os.path.exists(err_path):
                        try:
                            with open(err_path, "r", encoding="utf-8") as f:
                                total_errors += int(f.read().strip() or "0")
                        except Exception:
                            continue
                status["io_errors"] = total_errors
        except Exception:
            pass

        # detetar RO tentando escrita temporária
        test_dir = "/var/neurotron"
        try:
            Path(test_dir).mkdir(parents=True, exist_ok=True)
            with open(f"{test_dir}/rw_test.tmp", "w", encoding="utf-8") as f:
                f.write("ok")
            os.remove(f"{test_dir}/rw_test.tmp")
            status["read_only"] = False
        except Exception:
            status["read_only"] = True

        return status

    # ----------------------------------------------------------
    #  Classificação de Estado Cognitivo
    # ----------------------------------------------------------
    def classify_state(self, temp, jitter):
        if temp < 25:
            return "stable"
        elif temp < 45:
            return "warm"
        elif temp < 65:
            return "hot"
        elif temp < 85:
            return "critical"
        else:
            return "recovery"

    # ----------------------------------------------------------
    #  Eventos TeleMétricos V6
    # ----------------------------------------------------------
    def detect_events(self, raw, delta, accel, temp, jitter, fs):
        events = []

        # picos
        if delta["cpu"] > 15 or accel["cpu"] > 10:
            events.append("temp_rising_fast")

        # zona de stress
        if raw["cpu"] > 85:
            events.append("enter_stress_zone")

        # suspeita de loop (jitter anómalo)
        if self.jitter_avg > 0 and jitter > (self.jitter_avg * 3):
            events.append("loop_suspect")

        # fs
        if fs["read_only"] or (fs["io_errors"] or 0) > 0:
            events.append("fs_warning")

        # recuperação
        if self.last_state == "critical" and temp < 50:
            events.append("recovering")

        # exportar via callback
        if self.event_callback:
            for e in events:
                try:
                    self.event_callback(e, {
                        "raw": raw,
                        "delta": delta,
                        "accel": accel,
                        "temp": temp,
                        "jitter": jitter,
                        "fs": fs,
                    })
                except Exception:
                    # nunca quebrar o loop
                    pass

        return events

    # ----------------------------------------------------------
    #  STEP — chamada a cada ciclo cognitivo
    # ----------------------------------------------------------
    def step(self):
        """
        Faz um ciclo completo da Telemetria V6:
          - raw → delta → accel
          - jitter
          - temp
          - fs health
          - estado
          - eventos
        """

        raw = self.collect_raw()
        delta = self.compute_delta(raw)
        accel = self.compute_accel(delta)
        jitter = self.compute_jitter(raw["ts"])
        temp = self.compute_temperature(raw, delta)
        fs = self.compute_fs_health()

        state = self.classify_state(temp, jitter)
        events = self.detect_events(raw, delta, accel, temp, jitter, fs)

        self.last_state = state

        # Alimentar o dashboard via logbus.debug (pode ser desligado no futuro)
        try:
            logbus.debug(
                f"telemetry state={state} temp={temp:.1f} "
                f"cpu={raw['cpu']:.1f}% mem={raw['mem']:.1f}% "
                f"load={raw['load']:.2f} jitter={jitter:.3f}s"
            )
        except Exception:
            pass

        return {
            "raw": raw,
            "delta": delta,
            "accel": accel,
            "jitter": jitter,
            "temp": temp,
            "fs": fs,
            "state": state,
            "events": events,
        }


# Compatibilidade retro (cortex ainda importa TelemetryV5)
TelemetryV5 = TelemetryV6
