"""hud.py — a tela do Primo (macOS, PyObjC + WKWebView).

Uma janela GRANDE e normal (ocupa a área visível da tela, com os botões de
fechar/minimizar/redimensionar no canto, título transparente) que carrega hud.html:
calendário e compromissos de hoje, o robô 3D (é o próprio Primo; cai no SVG se o WebGL falhar),
clima, música com disco girando, anéis da máquina, rede e o visualizador de voz.
Abre no monitor onde está o mouse. Este arquivo só hospeda: os dados vêm de tela.py.

    entra (stdin):  mensagens do painel.py  ({"estado":..,"nivel":..,"transcricao":..})
    sai (stdout):   {"acao":"ptt"} quando a pessoa clica
Esconder: ESC, "esconde o painel" ou ctrl+opt+P (este precisa de Acessibilidade).
Teste sem voz:  python -m primo_voz.hud --demo
"""
from __future__ import annotations

import json
import math
import os
import queue
import subprocess
import sys
import threading
import time
from pathlib import Path

from primo_voz import identidade, tela as td
from primo_voz import painel as pn
from primo_voz.sons import Sons

AQUI = Path(__file__).resolve().parent


def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=None, chat=None) -> int:
    """entrada/saida/ao_iniciar/ao_sair: o modo app (app.py) liga o painel ao motor de voz sem passar por stdin/stdout.
    chat: o app.Chat; com ele a janela abre no chat.html e a voz (hud.html) fica atrás, trocada pelo botão de onda."""
    try:
        from AppKit import (NSApplication, NSApplicationActivationPolicyRegular, NSImage, NSMenu, NSMenuItem, NSBackingStoreBuffered, NSColor,
                            NSEvent, NSScreen, NSWindow)
        from Foundation import NSMakeRect, NSObject, NSTimer, NSURL
        import objc
        from WebKit import WKWebView, WKWebViewConfiguration, WKUserContentController
    except ImportError as e:
        sys.stderr.write(f"o painel precisa do PyObjC ({e.name})\n")
        return 3

    p = pn.Painel()
    fila: "queue.Queue[str]" = queue.Queue()
    sons = Sons(os.environ.get("PRIMOCODE_SEM_SONS") != "1")

    def emitir(obj: dict) -> None:
        if saida:
            saida(obj)
        else:
            sys.stdout.write(json.dumps(obj) + "\n")
            sys.stdout.flush()

    def js(msg: dict) -> None:
        fila.put("window.primo(" + json.dumps(msg, ensure_ascii=False) + ")")

    fila_chat: "queue.Queue[str]" = queue.Queue()
    fila_ui: "queue.Queue" = queue.Queue()          # o que mexe na janela, rodado na thread principal

    def js_chat(msg: dict) -> None:
        fila_chat.put("window.chat&&window.chat(" + json.dumps(msg, ensure_ascii=False) + ")")

    telas: dict = {}

    def mostrar(qual: str) -> None:
        """Troca entre o chat e a voz na mesma janela, sem recarregar nenhuma das duas."""
        if not telas:
            return
        telas["chat"].setHidden_(qual != "chat")
        telas["hud"].setHidden_(qual != "hud")
        telas["atual"] = qual
        telas["jan"].makeFirstResponder_(telas[qual])
        if qual == "hud":
            js({"comChat": True})
            js({"boot": True})
            if chat is not None:
                threading.Thread(target=chat.app.ligar_voz, daemon=True).start()
        elif chat is not None:                  # voltou pro chat: o microfone fecha
            threading.Thread(target=chat.app.desligar_voz, daemon=True).start()

    def painel_arquivos(pastas: bool):
        from AppKit import NSOpenPanel
        pn_ = NSOpenPanel.openPanel()
        pn_.setCanChooseFiles_(not pastas)
        pn_.setCanChooseDirectories_(pastas)
        pn_.setAllowsMultipleSelection_(not pastas)
        if pn_.runModal() != 1:
            return []
        return [str(u.path()) for u in pn_.URLs()]

    class Janela(NSWindow):
        def canBecomeKeyWindow(self):
            return True

        def canBecomeMainWindow(self):
            return True

    class Alvo(NSObject):
        def tick_(self, _):
            n = 0
            while n < 40:
                try:
                    cod = fila.get_nowait()
                except queue.Empty:
                    break
                self.web.evaluateJavaScript_completionHandler_(cod, None)
                n += 1
            while telas:
                try:
                    cod = fila_chat.get_nowait()
                except queue.Empty:
                    break
                telas["chat"].evaluateJavaScript_completionHandler_(cod, None)
            while True:
                try:
                    fila_ui.get_nowait()()
                except queue.Empty:
                    break
                except Exception as e:
                    sys.stderr.write(f"hud: {e}\n")
            if self.jan.isMiniaturized():
                return
            if p.visivel != bool(self.jan.isVisible()):
                if p.visivel:
                    self.jan.deminiaturize_(None)
                    self.jan.makeKeyAndOrderFront_(None)
                    js({"boot": True})
                    sons.tocar("boot")
                else:
                    self.jan.orderOut_(None)

    class Dono(NSObject):
        def windowShouldClose_(self, _):          # fechar o painel só esconde; o PrimoCode segue
            p.visivel = False
            return True

        def applicationShouldTerminate_(self, _a):
            try:                                          # quem pediu para sair? (vai para o log: ajuda a achar quem fecha o app)
                from Foundation import NSAppleEventManager
                ev = NSAppleEventManager.sharedAppleEventManager().currentAppleEvent()
                pid = ev.attributeDescriptorForKeyword_(0x73706964).int32Value() if ev is not None else 0
                quem = subprocess.run(["ps", "-p", str(pid), "-o", "command="], capture_output=True, text=True).stdout.strip()[:160] if pid else "interno"
                sys.stderr.write(f"hud: pedido para SAIR de pid={pid} ({quem})\n")
                sys.stderr.flush()
            except Exception as e:
                sys.stderr.write(f"hud: pedido para sair (não deu para ver quem: {e})\n")
            return 1

        def applicationWillTerminate_(self, _n):
            if ao_sair:
                try:
                    ao_sair()
                except Exception:
                    pass

        def applicationShouldHandleReopen_hasVisibleWindows_(self, _a, _v):   # clique no Dock traz de volta
            p.visivel = True
            return True

    class Msg(NSObject, protocols=[objc.protocolNamed("WKScriptMessageHandler")]):
        def userContentController_didReceiveScriptMessage_(self, ucc, msg):
            try:
                m = json.loads(str(msg.body()))
            except ValueError:
                return
            a = m.get("acao")
            if a == "erro":                     # erro de JavaScript da página: vai para o terminal
                sys.stderr.write("hud.html: " + str(m.get("arg")) + "\n")
                sys.stderr.flush()
            elif a == "sair":
                p.visivel = False
            elif a == "chat":                   # "Voltar pro chat", na tela de voz
                mostrar("chat")
            elif a in ("ptt", "entrar"):
                emitir({"acao": a})
            elif a == "print":                  # esconde o HUD para o print pegar a tela de verdade
                p.visivel = False
                threading.Timer(0.6, lambda: pn.executar_acao(m)).start()
                threading.Timer(2.2, lambda: setattr(p, "visivel", True)).start()
            elif td.acao(m):
                pass
            else:
                def _fazer():
                    pn.executar_acao(m)
                    if a == "midia":
                        time.sleep(0.4)
                        js({"musica": td.musica()})
                threading.Thread(target=_fazer, daemon=True).start()

    class MsgChat(NSObject, protocols=[objc.protocolNamed("WKScriptMessageHandler")]):
        """A ponte do chat.html. O que mexe na janela (trocar de tela, painel de arquivos, abrir link) é daqui; o resto vai para o app.Chat."""
        def userContentController_didReceiveScriptMessage_(self, ucc, msg):
            try:
                m = json.loads(str(msg.body()))
            except ValueError:
                return
            a = m.get("acao")
            if a == "erro":
                sys.stderr.write("chat.html: " + str(m.get("arg")) + "\n")
                sys.stderr.flush()
            elif a == "voz":                    # botão de onda: abre a voz e já começa a ouvir
                mostrar("hud")
                emitir({"acao": "ptt"})
            elif a == "anexar":
                caminhos = painel_arquivos(False)
                if caminhos:
                    threading.Thread(target=chat.anexar_caminhos, args=(caminhos,), daemon=True).start()
            elif a == "escolher_pasta":
                caminhos = painel_arquivos(True)
                if caminhos:
                    threading.Thread(target=chat.escolher_pasta, args=(caminhos[0],), daemon=True).start()
            elif a == "abrir_link":
                from AppKit import NSWorkspace
                u = str(m.get("url") or "")
                if u.startswith(("http://", "https://")):
                    NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(u))
            elif a == "haptico":                # a travadinha do esforço: o trackpad dá um toque em cada nível
                try:
                    from AppKit import NSHapticFeedbackManager
                    padrao = 2 if m.get("forte") else 1          # 1 = alinhamento (leve), 2 = troca de nível (mais firme)
                    NSHapticFeedbackManager.defaultPerformer().performFeedbackPattern_performanceTime_(padrao, 1)
                except Exception as e:
                    sys.stderr.write(f"hud: háptico: {e}\n")
            elif a in ("abrir_arquivo", "mostrar_finder"):     # só o que existe; o Mac abre com o app padrão
                from AppKit import NSWorkspace
                c = Path(os.path.expanduser(str(m.get("caminho") or "")))
                if c.is_absolute() and c.exists():
                    ws = NSWorkspace.sharedWorkspace()
                    if a == "abrir_arquivo":
                        ws.openURL_(NSURL.fileURLWithPath_(str(c)))
                    else:
                        ws.activateFileViewerSelectingURLs_([NSURL.fileURLWithPath_(str(c))])
            else:
                chat.receber(m)

    app = NSApplication.sharedApplication()
    app.setActivationPolicy_(NSApplicationActivationPolicyRegular)   # app de verdade: Dock e Cmd+Tab
    try:                                                              # nome e ícone do Primo, mesmo fora do bundle
        from AppKit import NSBundle
        info = NSBundle.mainBundle().infoDictionary()
        info["CFBundleName"] = info["CFBundleDisplayName"] = "Primo"
        icone = AQUI / "vendor" / "primo-icone.png"
        if icone.exists():
            app.setApplicationIconImage_(NSImage.alloc().initWithContentsOfFile_(str(icone)))
    except Exception as e:
        sys.stderr.write(f"hud: ícone: {e}\n")
    barra = NSMenu.alloc().init()
    raiz = NSMenuItem.alloc().init()
    barra.addItem_(raiz)
    menu = NSMenu.alloc().init()
    for rot, sel, tec in (("Sobre o Primo", "orderFrontStandardAboutPanel:", ""), (None, None, None),
                          ("Esconder o Primo", "hide:", "h"), (None, None, None), ("Sair do Primo", "terminate:", "q")):
        menu.addItem_(NSMenuItem.separatorItem() if rot is None else
                      NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(rot, sel, tec))
    raiz.setSubmenu_(menu)
    app.setMainMenu_(barra)
    mouse = NSEvent.mouseLocation()
    alvo_tela = next((s for s in NSScreen.screens() if s.frame().origin.x <= mouse.x < s.frame().origin.x + s.frame().size.width
                      and s.frame().origin.y <= mouse.y < s.frame().origin.y + s.frame().size.height), NSScreen.mainScreen())
    tela = alvo_tela.visibleFrame()          # maximizada, sem cobrir Dock nem barra de menu
    # título | fechar | minimizar | redimensionar | conteúdo sob a barra de título
    jan = Janela.alloc().initWithContentRect_styleMask_backing_defer_(tela, 1 | 2 | 4 | 8 | (1 << 15), NSBackingStoreBuffered, False)
    jan.setTitlebarAppearsTransparent_(True)
    jan.setTitleVisibility_(1)               # sem texto de título
    jan.setTitle_("Primo")
    jan.setReleasedWhenClosed_(False)
    dono = Dono.alloc().init()
    jan.setDelegate_(dono)
    app.setDelegate_(dono)
    jan.setMinSize_((900, 600))
    jan.setOpaque_(True)
    jan.setBackgroundColor_(NSColor.colorWithSRGBRed_green_blue_alpha_(0.043, 0.043, 0.039, 1.0))
    jan.setHasShadow_(False)
    cfg = WKWebViewConfiguration.alloc().init()
    cfg.preferences().setValue_forKey_(True, "allowFileAccessFromFileURLs")      # o hud.html lê o .glb local (vendor/)
    msg_h = Msg.alloc().init()
    cfg.userContentController().addScriptMessageHandler_name_(msg_h, "primo")
    web = WKWebView.alloc().initWithFrame_configuration_(NSMakeRect(0, 0, tela.size.width, tela.size.height), cfg)
    web.setAutoresizingMask_(2 | 16)
    web.setValue_forKey_(False, "drawsBackground")
    url = NSURL.fileURLWithPath_(str(AQUI / "hud.html"))
    web.loadFileURL_allowingReadAccessToURL_(url, NSURL.fileURLWithPath_(str(AQUI)))
    if chat is None:
        jan.setContentView_(web)
    else:                                   # o chat na frente; a voz atrás, já carregada (troca sem esperar)
        from AppKit import NSView
        fundo = NSView.alloc().initWithFrame_(NSMakeRect(0, 0, tela.size.width, tela.size.height))
        fundo.setAutoresizingMask_(2 | 16)
        cfg_c = WKWebViewConfiguration.alloc().init()
        cfg_c.preferences().setValue_forKey_(True, "allowFileAccessFromFileURLs")     # o vídeo/imagem/PDF citados tocam ali mesmo
        cfg_c.userContentController().addScriptMessageHandler_name_(MsgChat.alloc().init(), "chat")
        web_c = WKWebView.alloc().initWithFrame_configuration_(NSMakeRect(0, 0, tela.size.width, tela.size.height), cfg_c)
        web_c.setAutoresizingMask_(2 | 16)
        web_c.setValue_forKey_(False, "drawsBackground")
        # leitura da pasta pessoal: os arquivos que o agente cita (~/Primo/…) aparecem no chat sem servidor
        web_c.loadFileURL_allowingReadAccessToURL_(NSURL.fileURLWithPath_(str(AQUI / "chat.html")), NSURL.fileURLWithPath_(str(Path.home())))
        fundo.addSubview_(web)
        fundo.addSubview_(web_c)
        jan.setContentView_(fundo)
        telas.update({"chat": web_c, "hud": web, "jan": jan, "atual": "chat"})
        web.setHidden_(True)
        chat.js = js_chat
        chat.mostrar_voz = lambda: fila_ui.put(lambda: mostrar("hud"))    # um pedido falado com o chat na frente
    jan.makeKeyAndOrderFront_(None)
    app.activateIgnoringOtherApps_(True)
    alvo = Alvo.alloc().init()
    alvo.jan, alvo.web = jan, web
    NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(1 / 30, alvo, b"tick:", None, True)
    if chat is None:                        # sem chat a voz é a tela: o robô acorda com o som
        threading.Timer(1.2, lambda: (js({"nome": identidade.ler()["nome"]}), js({"robo": identidade.ler()["robo"]}), js({"boot": True}), sons.tocar("boot"))).start()
    else:                                   # com chat: só o nome e o robô, em silêncio; o boot fica pra quando abrir a voz
        threading.Timer(1.2, lambda: (js({"nome": identidade.ler()["nome"]}), js({"robo": identidade.ler()["robo"]}))).start()

    def esconder():
        p.visivel = False

    def tecla(ev):
        if ev.keyCode() == 53:                    # ESC: na voz com chat, volta pro chat; no chat, fica com a página
            if telas:
                if telas["atual"] == "hud":
                    mostrar("chat")
                    return None
                return ev
            esconder()
            return None
        return ev
    NSEvent.addLocalMonitorForEventsMatchingMask_handler_(1 << 10, tecla)

    def atalho(ev):
        f = ev.modifierFlags()
        if (f & 0x40000) and (f & 0x80000) and ev.keyCode() == 35:
            p.visivel = not p.visivel
    try:
        NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(1 << 10, atalho)
    except Exception:
        pass

    def medir():
        am = pn.Amostrador()
        cache: dict = {}
        k = 0
        while True:
            try:
                js({"maquina": td.maquina(am)})
                js({"musica": td.musica()})
                if k % 20 == 0:
                    js({"clima": td.clima_widget(cache)})
            except Exception as e:
                sys.stderr.write(f"hud: {e}\n")
            k += 1
            time.sleep(1)
    threading.Thread(target=medir, daemon=True).start()

    def agenda():            # em outra thread: o pedido de permissão do Calendário pode demorar
        evs: dict = {"permissao": "pendente", "eventos": []}
        k = 0
        falhas = 0
        while True:
            try:
                if k % 12 == 0 or evs["permissao"] != "ok":     # sem acesso, olha a cada 5 s (a pessoa pode ter liberado)
                    novo = td.eventos_do_mes()
                    if evs["permissao"] == "ok" and novo["permissao"] != "ok":
                        falhas += 1                              # erro transitório não derruba o cartão: segura os últimos eventos
                        if falhas >= 4:
                            evs, falhas = novo, 0
                    else:
                        evs, falhas = novo, 0
                # SEMPRE manda o estado junto: o painel nunca depende de uma mensagem que pode ter chegado antes da página
                js({"calendario": {"permissao": evs["permissao"], "dias": sorted({e["dia"] for e in evs["eventos"]})}})
                if evs["permissao"] == "ok":
                    js({"hoje": td.hoje_lista(evs["eventos"], time.time())})
            except Exception as e:
                sys.stderr.write(f"hud agenda: {e}\n")
            k += 1
            time.sleep(5)
    threading.Thread(target=agenda, daemon=True).start()

    def ler():
        for l in (entrada if entrada is not None else sys.stdin):
            try:
                m = json.loads(l)
            except ValueError:
                continue
            p.aplicar(m)
            js(m)
            if "estado" in m or "nivel" in m:
                js({"barras": td.barras(p.estado, p.nivel)})
        if entrada is None:
            os._exit(0)

    def encenar():
        roteiro = [("dormindo", "", "", 3), ("ouvindo", "roda os testes do projeto", "", 4),
                   ("pensando", "roda os testes do projeto", "", 3),
                   ("falando", "roda os testes do projeto", "Rodei. Passou tudo, 412 testes. Um aviso no painel.", 5)]
        while True:
            for est, tr, rs, dur in roteiro:
                p.aplicar({"estado": est})
                js({"estado": est, "transcricao": tr, "parcial": est == "ouvindo", "resposta": rs})
                fim = time.time() + dur
                while time.time() < fim:
                    v = abs(math.sin(time.time() * 9) * math.sin(time.time() * 2.3)) if est in ("ouvindo", "falando") else 0
                    js({"barras": td.barras(est, v)})
                    time.sleep(0.06)
                if est == "falando":
                    js({"evento": "sucesso"})
    if entrada is not None:
        threading.Thread(target=ler, daemon=True).start()
    elif "--livre" not in sys.argv:
        threading.Thread(target=encenar if demo else ler, daemon=True).start()
    if ao_iniciar:
        ao_iniciar(js, p)
    app.run()
    return 0


if __name__ == "__main__":
    sys.exit(main(demo="--demo" in sys.argv))
