"""painel.py — o que o HUD mostra, sem desenhar nada.

`Painel.aplicar(msg)` recebe uma linha do protocolo (JSON já lido) e atualiza o
quadro; hud.py (Mac, PyObjC) e `renderizar_tui` (qualquer terminal) só
DESENHAM este quadro. Assim a regra mora num lugar e se testa sem tela.

Mensagens aceitas (todas opcionais, uma por linha):
    {"estado": "ouvindo|pensando|falando|executando|dormindo|interrompido|esperando", "nivel": 0..1}
    {"transcricao": "texto", "parcial": true}
    {"resposta": "texto do painel"}
    {"info": {"projeto": "...", "branch": "...", "tokens": "12k", "plano": "..."}}
    {"painel": "esconder" | "mostrar" | "alternar"}
"""
from __future__ import annotations

import os
import re
import shutil
import subprocess
import sys
import time
from typing import Dict, List, Optional

ROTULOS = {
    "esperando": "PRONTO", "dormindo": "EM ESPERA", "ouvindo": "OUVINDO", "pensando": "PENSANDO",
    "falando": "FALANDO", "executando": "EXECUTANDO", "interrompido": "INTERROMPIDO",
}


class Painel:
    def __init__(self) -> None:
        self.estado = "esperando"
        self.nivel = 0.0
        self.transcricao = ""
        self.parcial = False
        self.resposta = ""
        self.nome = "Primo"
        self.visivel = True
        # só o que dá para pegar do computador da pessoa (ver coletar_tudo)
        self.info: Dict[str, str] = {"tocando": "", "app": "", "wifi": "", "clima": "", "evento": ""}
        self.sistema: Dict[str, Optional[float]] = {"cpu": None, "mem": None, "disco": None, "bat": None}

    def aplicar(self, msg: dict) -> None:
        if "estado" in msg and msg["estado"]:
            self.estado = str(msg["estado"])
        if "nivel" in msg and msg["nivel"] is not None:
            self.nivel = max(0.0, min(1.0, float(msg["nivel"])))
        if "transcricao" in msg:
            self.transcricao = str(msg["transcricao"] or "")
            self.parcial = bool(msg.get("parcial"))
            if not self.parcial and self.transcricao:
                self.resposta = ""        # pedido novo: a resposta velha sai
        if "resposta" in msg:
            self.resposta = str(msg["resposta"] or "")
        if msg.get("nome"):
            self.nome = str(msg["nome"])
        if isinstance(msg.get("info"), dict):
            for k, v in msg["info"].items():
                if k in self.info:
                    self.info[k] = str(v)
        p = msg.get("painel")
        if p == "esconder":
            self.visivel = False
        elif p == "mostrar":
            self.visivel = True
        elif p == "alternar":
            self.visivel = not self.visivel
        if msg.get("esconder"):
            self.visivel = False
        if msg.get("mostrar"):
            self.visivel = True

    @property
    def rotulo(self) -> str:
        return ROTULOS.get(self.estado, self.estado.upper())

    @property
    def cor(self) -> str:
        """ciano = a conversa (ouvir, falar); âmbar = o trabalho (pensar, executar)."""
        return "ambar" if self.estado in ("pensando", "executando", "interrompido") else "ciano"


# ── o que o computador está fazendo (sem dependência) ────────────────────

def _rodar(cmd: List[str], t: float = 3.0) -> str:
    try:
        return subprocess.run(cmd, capture_output=True, text=True, timeout=t).stdout
    except Exception:
        return ""


def coletar_sistema() -> Dict[str, Optional[float]]:
    """cpu, mem e bateria em 0..1 (None = não deu para medir)."""
    cpu = mem = bat = None
    try:
        n = os.cpu_count() or 1
        cpu = min(1.0, os.getloadavg()[0] / n)
    except Exception:
        pass
    try:
        import psutil  # opcional
        cpu = psutil.cpu_percent(interval=None) / 100.0
        mem = psutil.virtual_memory().percent / 100.0
        b = psutil.sensors_battery()
        bat = b.percent / 100.0 if b else None
        return {"cpu": cpu, "mem": mem, "bat": bat}
    except Exception:
        pass
    if shutil.which("vm_stat"):
        s = _rodar(["vm_stat"])
        pag = {k: int(v) for k, v in re.findall(r"Pages ([a-z ]+):\s+(\d+)", s)}
        usadas = pag.get("active", 0) + pag.get("wired down", 0) + pag.get("occupied by compressor", 0)
        total = usadas + pag.get("free", 0) + pag.get("inactive", 0) + pag.get("speculative", 0)
        mem = usadas / total if total else None
    if shutil.which("pmset"):
        m = re.search(r"(\d+)%", _rodar(["pmset", "-g", "batt"]))
        bat = int(m.group(1)) / 100.0 if m else None
    return {"cpu": cpu, "mem": mem, "bat": bat}


def _osa(script: str, t: float = 2.5) -> str:
    return _rodar(["osascript", "-e", script], t).strip()


def tocando() -> str:
    """Faixa — artista do Spotify ou do Music (só se o app já estiver aberto).
    "♪ " tocando, "⏸ " pausado. Quem toca ganha de quem só está pausado."""
    if sys.platform != "darwin":
        return ""
    pausado = ""
    for app in ("Spotify", "Music"):
        r = _osa(f'if application "{app}" is running then tell application "{app}" to return '
                 '(player state as string) & "|" & name of current track & " — " & artist of current track', 2.0)
        estado, _, faixa = r.partition("|")
        if not faixa:
            continue
        if estado == "playing":
            return faixa
        pausado = pausado or "⏸ " + faixa
    return pausado


def app_em_foco() -> str:
    if sys.platform != "darwin":
        return ""
    return _osa('tell application "System Events" to name of first application process whose frontmost is true')


def wifi() -> str:
    if sys.platform != "darwin":
        return ""
    for dev in ("en0", "en1"):
        r = _rodar(["networksetup", "-getairportnetwork", dev], 2.0)
        m = re.search(r"Network:\s*(.+)", r)
        if m:
            return m.group(1).strip()
    return ""


def clima(cache: dict) -> str:
    """wttr.in (grátis, sem chave), no máximo a cada 20 min. Offline = vazio."""
    if time.time() - cache.get("t", 0) < 1200:
        return cache.get("v", "")
    cache["t"] = time.time()
    try:
        import urllib.request
        with urllib.request.urlopen(urllib.request.Request(
                "https://wttr.in/?format=%t+%C&lang=pt", headers={"User-Agent": "curl/8"}), timeout=4) as r:
            v = r.read().decode("utf-8", "replace").strip()
        cache["v"] = v if v and len(v) < 40 and "Unknown" not in v else ""
    except Exception:
        cache["v"] = cache.get("v", "")
    return cache["v"]


def proximo_evento() -> str:
    """Próximo evento do Calendário. Só com PRIMOCODE_VOZ_CALENDARIO=1: o macOS pede permissão."""
    if sys.platform != "darwin" or os.environ.get("PRIMOCODE_VOZ_CALENDARIO") != "1":
        return ""
    return _osa('tell application "Calendar" to summary of (first event of calendar 1 '
                'whose start date > (current date))', 6)


def disco() -> Optional[float]:
    try:
        u = shutil.disk_usage("/")
        return u.used / u.total
    except Exception:
        return None


def coletar_tudo(cache: dict) -> dict:
    """Tudo o que o painel mostra do computador. Lento (osascript): rode em thread."""
    sis = coletar_sistema()
    sis["disco"] = disco()
    return {"sistema": sis, "info": {"tocando": tocando(), "app": app_em_foco(), "wifi": wifi(),
                                     "clima": clima(cache), "evento": proximo_evento()}}


# ── o painel no terminal (fora do Mac, ou sem PyObjC) ────────────────────

def _barra(v: Optional[float], n: int = 10) -> str:
    if v is None:
        return "·" * n
    cheio = int(round(max(0.0, min(1.0, v)) * n))
    return "█" * cheio + "░" * (n - cheio)


def renderizar_tui(p: Painel, largura: int = 56) -> str:
    """Um quadro de texto puro: cabe em qualquer terminal."""
    larg = max(40, largura)
    u = larg - 4
    anel = {"ouvindo": "(( ◉ ))", "pensando": "(( ◌ ))", "falando": "((( ◉ )))", "executando": "(( ▸ ))",
            "dormindo": "(  ·  )", "interrompido": "(( ✕ ))"}.get(p.estado, "(  ◉  )")

    def corta(t: str, n: int) -> str:
        t = " ".join(str(t).split())
        return t if len(t) <= n else t[: n - 1] + "…"

    def linha(t: str) -> str:
        return "│ " + corta(t, u).ljust(u) + " │"

    agora = time.strftime("%H:%M  %d/%m")
    s = p.sistema
    linhas = [
        "┌" + "─" * (larg - 2) + "┐",
        linha(f"{anel:<9} {p.nome.upper()} · {p.rotulo}".ljust(u - len(agora)) + agora),
        linha("você  › " + p.transcricao),
        linha(f"{p.nome.lower()[:5]:<5} › " + p.resposta),
        linha(f"cpu {_barra(s.get('cpu'), 8)} mem {_barra(s.get('mem'), 8)} disco {_barra(s.get('disco'), 8)}"),
        linha(f"bat {_barra(s.get('bat'), 8)}  wifi {p.info.get('wifi') or '-'}  {p.info.get('clima') or ''}"),
        linha("♪ " + (p.info.get("tocando") or "—") + ("   ▢ " + p.info["app"] if p.info.get("app") else "")),
        "└" + "─" * (larg - 2) + "┘",
    ]
    return "\n".join(linhas)


def tui_laco(entrada, saida=None, largura: int = 56) -> None:
    """Lê o protocolo e redesenha o quadro no lugar. Para o `/voice painel` fora do Mac."""
    import sys
    saida = saida or sys.stdout
    p = Painel()
    ultimo = 0.0
    cache: dict = {}
    cache_t = [0.0]
    for linha in entrada:
        try:
            import json
            p.aplicar(json.loads(linha))
        except ValueError:
            continue
        if time.time() - ultimo > 0.2:
            if time.time() - cache_t[0] > 5:
                r = coletar_tudo(cache)
                p.sistema, p.info = r["sistema"], {**p.info, **r["info"]}
                cache_t[0] = time.time()
            saida.write("\x1b[H\x1b[J" + renderizar_tui(p, largura) + "\n")
            saida.flush()
            ultimo = time.time()


# ── amostras ao vivo para o HUD de tela cheia ────────────────────────────

class Amostrador:
    """CPU por núcleo e tráfego de rede, por diferença entre duas leituras."""

    def __init__(self) -> None:
        self._cpu = None
        self._rede = None
        self._lib = None

    def _ticks(self):
        import ctypes
        from ctypes import POINTER, byref, c_int, c_uint
        if self._lib is None:
            self._lib = ctypes.CDLL("/usr/lib/libSystem.B.dylib")
            self._lib.mach_host_self.restype = c_uint
        n, info, cnt = c_uint(), POINTER(c_int)(), c_uint()
        if self._lib.host_processor_info(self._lib.mach_host_self(), 2, byref(n), byref(info), byref(cnt)) != 0:
            return None
        ticks = [tuple(info[i * 4 + k] & 0xFFFFFFFF for k in range(4)) for i in range(n.value)]
        try:
            self._lib.vm_deallocate.argtypes = [c_uint, ctypes.c_void_p, ctypes.c_size_t]
            self._lib.vm_deallocate(c_uint.in_dll(self._lib, "mach_task_self_").value,
                                    ctypes.cast(info, ctypes.c_void_p), cnt.value * 4)
        except Exception:
            pass
        return ticks

    def cores(self) -> List[float]:
        if sys.platform != "darwin":
            return []
        try:
            atual = self._ticks()
        except Exception:
            return []
        antes, self._cpu = self._cpu, atual
        if not atual or not antes or len(antes) != len(atual):
            return [0.0] * len(atual or [])
        saida = []
        for a, b in zip(antes, atual):
            d = [(y - x) & 0xFFFFFFFF for x, y in zip(a, b)]
            total = sum(d)
            saida.append(0.0 if not total else 1.0 - d[2] / total)
        return saida

    def rede(self) -> Dict[str, float]:
        """bytes/s de entrada e saída (soma das interfaces en*)."""
        s = _rodar(["netstat", "-ib"], 2.0)
        linhas = s.splitlines()
        if not linhas:
            return {"up": 0.0, "down": 0.0}
        cab = linhas[0].split()
        try:
            ii, oi = cab.index("Ibytes"), cab.index("Obytes")
        except ValueError:
            return {"up": 0.0, "down": 0.0}
        vistos, ent, sai = set(), 0, 0
        for l in linhas[1:]:
            c = l.split()
            if len(c) > max(ii, oi) and c[0].startswith("en") and c[0] not in vistos and "<Link" in l:
                vistos.add(c[0])
                try:
                    ent += int(c[ii])
                    sai += int(c[oi])
                except ValueError:
                    pass
        agora = time.time()
        antes, self._rede = self._rede, (agora, ent, sai)
        if not antes or agora <= antes[0]:
            return {"up": 0.0, "down": 0.0}
        dt = agora - antes[0]
        return {"down": max(0, ent - antes[1]) / dt, "up": max(0, sai - antes[2]) / dt}


def carregando() -> bool:
    return "AC Power" in _rodar(["pmset", "-g", "batt"], 2.0) if sys.platform == "darwin" else False


def capa_da_faixa() -> str:
    """URL da capa no Spotify (o Music não expõe URL)."""
    if sys.platform != "darwin":
        return ""
    return _osa('if application "Spotify" is running then tell application "Spotify" to return artwork url of current track', 2.0)


# ── extras do HUD de tela cheia: apps do Dock, rede, tempo, mídia ────────

def apps_do_dock() -> List[str]:
    """Os apps fixos no Dock da pessoa (nomes), via defaults + plutil."""
    if sys.platform != "darwin":
        return []
    import plistlib
    r = subprocess.run(["defaults", "export", "com.apple.dock", "-"], capture_output=True, timeout=3)
    try:
        j = plistlib.loads(r.stdout)
    except Exception:
        return []
    nomes = []
    for it in j.get("persistent-apps", []):
        d = it.get("tile-data", {})
        n = d.get("file-label")
        if n and n not in ("Launchpad", "Mission Control", "Apps"):
            nomes.append(n)
    return nomes


def ip_local() -> str:
    try:
        import socket
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("10.255.255.255", 1))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return ""


def uptime_s() -> int:
    m = re.search(r"sec = (\d+)", _rodar(["sysctl", "-n", "kern.boottime"], 2.0))
    return int(time.time() - int(m.group(1))) if m else 0


def _json_url(url: str, t: float = 5.0):
    import json as _j
    import urllib.request
    with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "primocode"}), timeout=t) as r:
        return _j.loads(r.read().decode("utf-8", "replace"))


def local_e_clima3(cache: dict) -> dict:
    """IP público, cidade e previsão de 3 dias. ipapi.co + open-meteo: grátis, sem chave. Cache de 30 min."""
    if time.time() - cache.get("t3", 0) < 1800 and "v3" in cache:
        return cache["v3"]
    cache["t3"] = time.time()
    v = cache.get("v3", {})
    try:
        g = _json_url("https://ipapi.co/json/")
        lat, lon = float(g["latitude"]), float(g["longitude"])
        v = {"ip_publico": g.get("ip", ""), "cidade": g.get("city", ""), "lat": lat, "lon": lon}
        f = _json_url(f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}"
                      "&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto&forecast_days=3")
        d = f["daily"]
        v["previsao"] = [{"dia": d["time"][i], "max": round(d["temperature_2m_max"][i]), "min": round(d["temperature_2m_min"][i]),
                          "cod": d["weather_code"][i]} for i in range(len(d["time"]))]
    except Exception:
        pass
    cache["v3"] = v
    return v


def midia() -> dict:
    """Spotify (ou Music): estado, posição e duração em segundos."""
    if sys.platform != "darwin":
        return {}
    for app in ("Spotify", "Music"):
        div = "1000" if app == "Spotify" else "1"
        r = _osa(f'if application "{app}" is running then tell application "{app}" to return (player state as string) & "|" '
                 f'& (player position as string) & "|" & ((duration of current track) as string)', 2.0)
        p = r.split("|")
        if len(p) == 3:
            try:
                return {"app": app, "estado": p[0], "pos": float(p[1].replace(",", ".")),
                        "dur": float(p[2].replace(",", ".")) / float(div)}
            except ValueError:
                continue
    return {}


def controle_midia(acao: str) -> None:
    cmd = {"anterior": "previous track", "tocar": "playpause", "proxima": "next track"}.get(acao)
    if not cmd or sys.platform != "darwin":
        return
    for app in ("Spotify", "Music"):
        if _osa(f'application "{app}" is running') == "true":
            _osa(f'tell application "{app}" to {cmd}')
            return


def executar_acao(m: dict) -> None:
    """Ações dos botões do HUD. Tudo real; sem Mac, não faz nada."""
    a, arg = m.get("acao"), str(m.get("arg") or "")
    if sys.platform != "darwin":
        return
    if a == "app" and arg:
        subprocess.Popen(["open", "-a", arg])
    elif a == "midia":
        controle_midia(arg)
    elif a == "volume":
        d = 10 if arg == "mais" else -10
        _osa(f"set volume output volume ((output volume of (get volume settings)) + ({d}))")
    elif a == "travar":
        subprocess.Popen(["pmset", "displaysleepnow"])
    elif a == "foco":
        subprocess.Popen(["open", "x-apple.systempreferences:com.apple.Focus-Settings.extension"])
    elif a == "print":
        pasta = os.path.expanduser("~/Desktop")
        subprocess.Popen(["screencapture", "-x", os.path.join(pasta, "primo-" + time.strftime("%Y%m%d-%H%M%S") + ".png")])
