#!/usr/bin/env bash
# JARVIS — lanzador del asistente local (Newelle)
#
#   sin argumentos   abre JARVIS a pantalla completa, sin pasar por el chat
#   --chat           abre Newelle normal, con sus pestañas y su historial
#   --stop           la cierra y libera el modelo
#   --status         que corre, con que cerebro y cuanto ocupa
#
# El lanzador NO decide el motor: lo lee de gsettings (language-model) y se
# adapta. Asi no hay que tocarlo cada vez que se cambia de cerebro, que es
# justo lo que lo dejo desincronizado antes: precargaba Ollama mientras la
# app usaba su llamacpp interno, ocupando 2.9 GB de VRAM para nada.
#
# Y el MODELO tampoco, por lo mismo. Estaba escrito a mano aqui y volvio a
# pasar exactamente lo de antes: al cambiar la app a qwen3.5:4b-jarvis, el
# lanzador seguia precargando qwen3.5:4b. Los dos no caben en 3717 MiB, asi
# que ollama tenia que descargar uno para cargar el otro con el panel ya
# esperando: 6,55 s hasta la primera respuesta contra 3,70 con los dos de
# acuerdo. El sintoma no era "va lento", era "no se abre el panel solo".

set -uo pipefail

APPID=io.github.qwersyk.Newelle
OLLAMA_URL=http://localhost:11434

# La salida de la app iba entera a /dev/null. Cuando el panel dejo de abrirse,
# el traceback que lo explicaba —una linea— no estaba en ningun sitio, y el
# fallo se diagnostico dos veces mal antes de mirarlo. Ahora se guarda.
# misma idea que en los scripts de voz: la raiz sale de donde vive esto
RAIZ=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)
REGISTRO=$RAIZ/jarvis-app.log
: > "$REGISTRO" 2>/dev/null || REGISTRO=/dev/null
GGUF=$HOME/.var/app/$APPID/config/models/custom_models/qwen3.5-4b.gguf

# Elige rama: //master es la version propia (jarvis-skin, con reactor arc);
# //stable es la de Flathub, la red de seguridad.
#
# El chequeo mira la typelib de Vte en lib/: si un build deja mal el libdir,
# la app arranca y muere con "Namespace Vte not available". Asi el icono
# nunca abre una version rota, y se pasa solo a la propia cuando este sana.
pick_branch() {
    local base=$HOME/.local/share/flatpak/app/$APPID/x86_64
    if [ -f "$base/master/active/files/lib/girepository-1.0/Vte-3.91.typelib" ]; then
        echo "master"
    else
        echo "stable"
    fi
}
RAMA=$(pick_branch)

motor() {
    flatpak run --command=gsettings "$APPID//$RAMA" get "$APPID" language-model 2>/dev/null \
      | tr -d "'" | tr -d '\n'
}

# El modelo que la app tiene elegido de verdad, leido de sus ajustes. Si no se
# puede leer se cae al 4b pelado, que es mejor que precargar algo que no existe.
modelo() {
    flatpak run --command=gsettings "$APPID//$RAMA" get "$APPID" llm-settings 2>/dev/null \
      | python3 -c "
import json, sys
s = sys.stdin.read().strip()
try:
    d = json.loads(s[1:-1].replace('\\\\\\\\', '\\\\').replace(\"\\\\'\", \"'\"))
    print(d.get('ollama', {}).get('model') or 'qwen3.5:4b')
except Exception:
    print('qwen3.5:4b')
" 2>/dev/null || echo qwen3.5:4b
}
MODEL=$(modelo)

notify() { command -v notify-send >/dev/null && notify-send -i jarvis "JARVIS" "$1"; }

ollama_up() { curl -s --max-time 2 "$OLLAMA_URL/api/version" >/dev/null 2>&1; }

app_corriendo() { pgrep -f "bin/newelle" >/dev/null; }

# El llama-server interno de la app, no el de Ollama: el suyo vive en /app/bin.
servidor_interno() { pgrep -f "/app/bin/llama-server" | head -1; }

case "${1:-start}" in
  --stop)
    # flatpak kill y no pkill: el patron "bin/newelle" tambien casa con la
    # propia linea de comandos cuando esto se invoca desde un bash -c inline,
    # y el script se mataba a si mismo antes de llegar aqui.
    flatpak kill "$APPID" 2>/dev/null || pkill -f "bin/newelle" 2>/dev/null
    if [ "$(motor)" = "ollama" ] && ollama_up; then
      curl -s "$OLLAMA_URL/api/generate" \
        -d "{\"model\":\"$MODEL\",\"keep_alive\":0}" >/dev/null 2>&1
      notify "Detenido, VRAM liberada"
    else
      notify "Detenido"
    fi
    echo "JARVIS detenido."
    ;;

  --status|--status-notify)
    MOTOR=$(motor)
    app_corriendo && S1="app: abierta (//$RAMA)" || S1="app: cerrada"
    S2="cerebro: ${MOTOR:-?}"

    if [ "$MOTOR" = "ollama" ]; then
      S3=$(curl -s "$OLLAMA_URL/api/ps" 2>/dev/null \
        | python3 -c 'import json,sys
ms = json.load(sys.stdin).get("models", [])
print("\n".join("cargado: %s  %.1f GB (%.1f en GPU)"
      % (m["name"], m.get("size",0)/1e9, m.get("size_vram",0)/1e9) for m in ms)
      or "sin modelos cargados")' 2>/dev/null)
      [ -n "$S3" ] || S3="ollama: caido"
    else
      PID=$(servidor_interno)
      if [ -n "$PID" ]; then
        S3="cargado en RAM ($(awk '/VmRSS/{printf "%.1f GB", $2/1048576}' \
             "/proc/$PID/status" 2>/dev/null))"
      else
        S3="sin modelos cargados"
      fi
    fi

    S4=$(nvidia-smi --query-gpu=memory.used,memory.free --format=csv,noheader 2>/dev/null \
      | sed 's/^/VRAM usada\/libre: /')

    if [ "$1" = "--status-notify" ]; then
      notify "$S1 · $S2"$'\n'"$S3"$'\n'"$S4"
    else
      printf '%s\n%s\n\n%s\n%s\n' "$S1" "$S2" "$S3" "$S4"
    fi
    ;;

  *)
    MOTOR=$(motor)

    if [ "$MOTOR" = "ollama" ]; then
      if ! ollama_up; then
        systemctl start ollama 2>/dev/null || sudo systemctl start ollama
        for _ in $(seq 20); do ollama_up && break; sleep 0.5; done
      fi
      ollama_up || { notify "Ollama no arranca"; echo "ERROR: ollama no responde" >&2; exit 1; }
      # precargar en GPU para que la primera respuesta no tarde.
      # think:false o se va a 40 s pensando en voz alta.
      curl -s "$OLLAMA_URL/api/generate" \
        -d "{\"model\":\"$MODEL\",\"prompt\":\"ok\",\"stream\":false,\"think\":false,\"keep_alive\":\"30m\"}" \
        >/dev/null 2>&1 &
    else
      [ -f "$GGUF" ] || { notify "Falta el modelo: $(basename "$GGUF")"
                          echo "ERROR: no esta $GGUF" >&2; exit 1; }
    fi

    # --chat abre Newelle tal cual; sin argumentos va derecho al asistente
    DIRECTO=si
    [ "${1:-}" = "--chat" ] && DIRECTO=no

    if app_corriendo; then
      command -v xdotool >/dev/null && \
        xdotool search --name "^newelle$" windowactivate 2>/dev/null
    elif [ "$DIRECTO" = "no" ]; then
      exec flatpak run "$APPID//$RAMA"
    else
      # En segundo plano y no con exec: hay que esperar a que exista la ventana
      # antes de pedir la pestaña. --run-action se procesa antes de que la
      # ventana este construida, asi que lanzarlo de golpe no serviria.
      #
      # Y esperar al PROCESO no basta: el proceso existe en cuanto python
      # arranca, varios segundos antes de que haya ventana. Con `sleep 3` a
      # ojo, un arranque un poco mas lento pierde la carrera y la accion
      # revienta con "'MyApp' object has no attribute 'win'" — la app abre,
      # pero en el chat y sin panel. Se espera a la VENTANA, que es la
      # condicion de verdad.
      flatpak run "$APPID//$RAMA" >>"$REGISTRO" 2>&1 &
      for _ in $(seq 60); do
        if command -v xdotool >/dev/null; then
          xdotool search --name "^newelle$" >/dev/null 2>&1 && break
        else
          pgrep -f "bin/newelle" >/dev/null && break
        fi
        sleep 0.5
      done
      # sin xdotool no hay forma de saberlo: se vuelve al margen a ojo
      command -v xdotool >/dev/null || sleep 3
    fi

    if [ "$DIRECTO" = "si" ]; then
      # jarvis_full y no jarvis+hud: el segundo alterna, asi que pulsar el
      # icono con la app ya abierta te sacaria de pantalla completa.
      flatpak run "$APPID//$RAMA" --run-action=jarvis_full >>"$REGISTRO" 2>&1
    fi
    ;;
esac
