"""tela.py — os dados REAIS da tela do Primo (calendário, clima, música, máquina) e as regras pequenas dela.

Nada aqui desenha. hud.py chama estas funções numa thread e empurra o resultado para hud.html;
a lógica que não depende de tela (texto de "em X min", troca de robô, quem fala nas barras) se testa sem microfone.
"""
from __future__ import annotations

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

from primo_voz import identidade
from primo_voz import painel as pn


# ── regras puras ─────────────────────────────────────────────────────────

def em_x_min(inicio: float, agora: float) -> str:
    """'em 25 min', 'em 1 h 10 min', 'agora' (começou há pouco), 'amanhã' passa da meia-noite."""
    d = int((inicio - agora) // 60)
    if d < -5:
        return "já começou"
    if d <= 0:
        return "agora"
    if d < 60:
        return f"em {d} min"
    h, m = divmod(d, 60)
    if h >= 24:
        return "amanhã"
    return f"em {h} h" + (f" {m} min" if m else "")


def barras(estado: str, nivel: float) -> Dict[str, object]:
    """Quem fala e quão alto: a PESSOA (ciano) em 'ouvindo' e em espera (o mic escuta); o PRIMO (verde) só em 'falando'. O resto é silêncio."""
    n = max(0.0, min(1.0, float(nivel or 0)))
    if estado in ("ouvindo", "dormindo"):
        return {"quem": "pessoa", "nivel": n}
    if estado == "falando":
        return {"quem": "primo", "nivel": n}
    return {"quem": None, "nivel": 0.0}


def nivel_do_mic(rms: float) -> float:
    return max(0.0, min(1.0, rms / 4000.0))


def envelope(arquivo, janela: float = 0.05) -> List[float]:
    """Volume real da fala do Primo (0..1 a cada 50 ms): converte o mp3 em wav com afconvert e mede. Sem Mac = []."""
    if sys.platform != "darwin" or not shutil.which("afconvert"):
        return []
    import array
    import wave
    tmp = tempfile.mktemp(suffix=".wav")
    try:
        subprocess.run(["afconvert", "-f", "WAVE", "-d", "LEI16@16000", "-c", "1", str(arquivo), tmp],
                       capture_output=True, timeout=10, check=True)
        with wave.open(tmp) as w:
            dados = array.array("h", w.readframes(w.getnframes()))
            taxa = w.getframerate()
        passo = max(1, int(taxa * janela))
        env = []
        for i in range(0, len(dados), passo):
            b = dados[i:i + passo]
            env.append((sum(x * x for x in b) / len(b)) ** 0.5 if b else 0.0)
        pico = max(env) if env else 0
        return [round(min(1.0, e / pico * 1.1), 3) for e in env] if pico else []
    except Exception:
        return []
    finally:
        try:
            os.unlink(tmp)
        except OSError:
            pass


def hoje_lista(eventos: List[dict], agora: float) -> List[dict]:
    """Os eventos de HOJE que ainda importam (não acabaram), o primeiro marcado como próximo, com 'em X min'."""
    t = time.localtime(agora)
    out = []
    for e in eventos:
        if time.localtime(e["inicio"]).tm_mday != t.tm_mday or e["fim"] <= agora and not e.get("dia_todo"):
            continue
        item = {"titulo": e["titulo"], "hora": "dia todo" if e.get("dia_todo") else time.strftime("%H:%M", time.localtime(e["inicio"])),
                "em": "" if e.get("dia_todo") else em_x_min(e["inicio"], agora), "prox": False}
        out.append(item)
    for item in out:
        if item["hora"] != "dia todo":
            item["prox"] = True
            break
    return out


# ── calendário (EventKit; pede permissão uma vez) ────────────────────────

_ek: dict = {}


def _store():
    if "s" not in _ek:
        from EventKit import EKEventStore
        _ek["s"] = EKEventStore.alloc().init()
    return _ek["s"]


def calendario_permissao() -> str:
    """ok | pendente | negada | sem (sem EventKit/sem Mac)."""
    if sys.platform != "darwin":
        return "sem"
    try:
        from EventKit import EKEventStore
    except ImportError:
        return "sem"
    st = int(EKEventStore.authorizationStatusForEntityType_(0))
    # 4 = só escrita (um pedido antigo concedeu menos do que o Primo precisa): o macOS não deixa "subir" por código,
    # então tratamos como pendente e o pedido zera a decisão do próprio app antes de pedir o acesso completo.
    return {3: "ok", 0: "pendente", 4: "pendente", 1: "negada", 2: "negada"}.get(st, "negada")


def pedir_calendario() -> None:
    """Dispara o pedido do macOS (uma vez). Não bloqueia. O resultado vai para o log (stderr)."""
    if time.time() - _ek.get("pedindo", 0) < 120:      # o aviso do macOS já está na tela esperando resposta
        return
    _ek["pedindo"] = time.time()

    def fim(ok, err):
        _ek["pedindo"] = 0
        sys.stderr.write(f"calendario: pedido respondido ok={bool(ok)} erro={err} status={calendario_permissao()}\n")
        sys.stderr.flush()
    try:
        from EventKit import EKEventStore
        if int(EKEventStore.authorizationStatusForEntityType_(0)) == 4:
            r = subprocess.run(["tccutil", "reset", "Calendar", "ai.primocode.primo"], capture_output=True, text=True)
            sys.stderr.write(f"calendario: acesso só de escrita; zerando a decisão do Primo ({r.returncode})\n")
            _ek.pop("s", None)
        s = _store()
        sys.stderr.write(f"calendario: pedindo acesso (status antes={calendario_permissao()}, bundle={_bundle()})\n")
        sys.stderr.flush()
        if hasattr(s, "requestFullAccessToEventsWithCompletion_"):
            s.requestFullAccessToEventsWithCompletion_(fim)
        else:
            s.requestAccessToEntityType_completion_(0, fim)
    except Exception as e:
        sys.stderr.write(f"calendario: falhou ao pedir: {e}\n")


def _bundle() -> str:
    try:
        from Foundation import NSBundle
        return str(NSBundle.mainBundle().bundleIdentifier())
    except Exception:
        return "?"


def eventos_do_mes(agora: Optional[float] = None) -> dict:
    """{'permissao','eventos':[{titulo,inicio,fim,dia_todo,dia}]} do mês inteiro (hoje e dias com ponto)."""
    perm = calendario_permissao()
    if perm == "pendente":
        pedir_calendario()
    if perm != "ok":
        return {"permissao": perm, "eventos": []}
    from Foundation import NSDate
    agora = agora or time.time()
    t = time.localtime(agora)
    ini = time.mktime((t.tm_year, t.tm_mon, 1, 0, 0, 0, 0, 0, -1))
    fim = time.mktime((t.tm_year + (t.tm_mon == 12), t.tm_mon % 12 + 1, 1, 0, 0, 0, 0, 0, -1))
    s = _store()
    pred = s.predicateForEventsWithStartDate_endDate_calendars_(NSDate.dateWithTimeIntervalSince1970_(ini),
                                                                NSDate.dateWithTimeIntervalSince1970_(fim), None)
    out = []
    for e in sorted(s.eventsMatchingPredicate_(pred) or [], key=lambda e: e.startDate().timeIntervalSince1970()):
        a, b = e.startDate().timeIntervalSince1970(), e.endDate().timeIntervalSince1970()
        out.append({"titulo": str(e.title() or "(sem título)"), "inicio": a, "fim": b, "dia_todo": bool(e.isAllDay()),
                    "dia": time.localtime(a).tm_mday})
    return {"permissao": perm, "eventos": out}


# ── clima (open-meteo, cidade por IP, sem chave) ─────────────────────────

_COD = [((0,), "Céu limpo", "☀︎"), ((1, 2), "Poucas nuvens", "⛅︎"), ((3,), "Nublado", "☁︎"), ((45, 48), "Neblina", "≋"),
        ((51, 53, 55, 56, 57), "Garoa", "☂︎"), ((61, 63, 65, 66, 67, 80, 81, 82), "Chuva", "☂︎"),
        ((71, 73, 75, 77, 85, 86), "Neve", "❄︎"), ((95, 96, 99), "Tempestade", "⚡︎")]


def condicao(cod: int):
    for cods, txt, ico in _COD:
        if cod in cods:
            return txt, ico
    return "—", "·"


def clima_widget(cache: dict) -> dict:
    """cidade, temperatura agora, condição, máx/mín e os próximos 4 dias. Cache de 20 min; offline = o último."""
    if time.time() - cache.get("tw", 0) < 1200 and "w" in cache:
        return cache["w"]
    cache["tw"] = time.time()
    v = cache.get("w", {})
    try:
        g = pn._json_url("https://ipapi.co/json/")
        lat, lon = float(g["latitude"]), float(g["longitude"])
        f = pn._json_url(f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}"
                         "&current=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min"
                         "&timezone=auto&forecast_days=5")
        d, c = f["daily"], f["current"]
        txt, ico = condicao(int(c["weather_code"]))
        v = {"cidade": g.get("city", ""), "temp": round(c["temperature_2m"]), "cond": txt, "icone": ico,
             "max": round(d["temperature_2m_max"][0]), "min": round(d["temperature_2m_min"][0]),
             "dias": [{"data": d["time"][i], "max": round(d["temperature_2m_max"][i]),
                       "icone": condicao(int(d["weather_code"][i]))[1]} for i in range(1, len(d["time"]))]}
    except Exception:
        pass
    cache["w"] = v
    return v


# ── música (Spotify ou Music) ────────────────────────────────────────────

_capa: dict = {}


def _capa_music() -> str:
    """O Music não dá URL: grava a arte da faixa num arquivo temporário e devolve como data URI."""
    tmp = os.path.join(tempfile.gettempdir(), "primo-capa.dat")
    r = pn._osa('tell application "Music" to tell artwork 1 of current track to set d to raw data\n'
                f'set f to open for access POSIX file "{tmp}" with write permission\nset eof f to 0\nwrite d to f\nclose access f', 4)
    try:
        b = open(tmp, "rb").read()
        os.unlink(tmp)
    except OSError:
        return ""
    if not b:
        return ""
    tipo = "image/png" if b[:4] == b"\x89PNG" else "image/jpeg"
    return f"data:{tipo};base64," + base64.b64encode(b).decode()


def musica() -> dict:
    """{'app','estado','faixa','artista','pos','dur','capa'}; {} se nada está aberto. A capa só é buscada quando a faixa muda."""
    if sys.platform != "darwin":
        return {}
    for app in ("Spotify", "Music"):
        div = 1000.0 if app == "Spotify" else 1.0
        art = ' & "|" & (artwork url of current track)' if app == "Spotify" else ""
        r = pn._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) & "|" & name of current track & "|" '
                    f'& artist of current track{art}', 2.5)
        p = r.split("|")
        if len(p) < 5:
            continue
        try:
            pos, dur = float(p[1].replace(",", ".")), float(p[2].replace(",", ".")) / div
        except ValueError:
            continue
        chave = (app, p[3], p[4])
        if _capa.get("k") != chave:
            _capa["k"] = chave
            _capa["v"] = p[5] if app == "Spotify" and len(p) > 5 else (_capa_music() if app == "Music" else "")
        return {"app": app, "estado": p[0], "faixa": p[3], "artista": p[4], "pos": pos, "dur": dur, "capa": _capa.get("v", "")}
    return {}


def controle(acao: str) -> None:
    pn.controle_midia(acao)


# ── a máquina ────────────────────────────────────────────────────────────

def memoria() -> dict:
    """{'usada_gb','total_gb','pct'} (usada = ativa + fixa + comprimida, como o painel antigo)."""
    try:
        total = int(pn._rodar(["sysctl", "-n", "hw.memsize"], 2.0).strip())
        s = pn._rodar(["vm_stat"], 2.0)
        tam = int(re.search(r"page size of (\d+)", s).group(1))
        pag = {k: int(v) for k, v in re.findall(r"Pages ([a-z ]+):\s+(\d+)", s)}
        usada = (pag.get("active", 0) + pag.get("wired down", 0) + pag.get("occupied by compressor", 0)) * tam
        return {"usada_gb": round(usada / 1e9, 1), "total_gb": round(total / 1e9), "pct": round(100 * usada / total)}
    except Exception:
        return {}


def disco() -> dict:
    try:
        u = shutil.disk_usage("/")
        return {"pct": round(100 * u.used / u.total), "livre_gb": round(u.free / 1e9)}
    except Exception:
        return {}


def bateria() -> dict:
    s = pn._rodar(["pmset", "-g", "batt"], 2.0)
    m = re.search(r"(\d+)%", s)
    if not m:
        return {}
    return {"pct": int(m.group(1)), "tomada": "AC Power" in s}


def maquina(am: "pn.Amostrador") -> dict:
    cores = [round(x, 3) for x in am.cores()]
    r = {"cpu": round(100 * sum(cores) / len(cores)) if cores else 0, "nucleos": len(cores) or (os.cpu_count() or 0),
         "mem": memoria(), "disco": disco(), "bat": bateria()}
    r.update({k: round(v, 1) for k, v in am.rede().items()})
    return r


def acao(m: dict) -> bool:
    """Ações da tela que só ela entende. True = tratada."""
    a, arg = m.get("acao"), str(m.get("arg") or "")
    if a == "calendario":
        if calendario_permissao() == "pendente":
            pedir_calendario()                        # o pedido do macOS sai com o nome do app (Primo)
        else:
            subprocess.Popen(["open", "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars"])
        return True
    return False
