#!/bin/bash
# ============================================================
# ScreenAR -- Instalador automatico para Raspberry Pi
#
# Provisiona una Raspberry Pi OS Lite recien flasheada como reproductor
# ScreenAR completo: Chromium en modo kiosco + arranque automatico + los 6
# servicios reales que corren en produccion (cache offline, file server local
# con range requests, watchdog de red, watchdog de chromium, control remoto
# por polling, boton fisico de reset). Este archivo es la UNICA fuente de
# verdad -- los scripts que instala se generan a partir de raspberry-pi/scripts/
# en el repo (ver docs/05_PLAYERS.md). No edites los heredocs de este archivo
# a mano -- se regenera con "node raspberry-pi/generate-install.js" a partir
# de raspberry-pi/scripts/*, que es la copia legible/editable de verdad.
#
# Uso: curl -s https://tu-servidor/public/downloads/install-raspberry-pi.sh | sudo bash
# O:   sudo bash install.sh --api https://tu-servidor
# ============================================================

set -e

API_URL=""
INSTALL_DIR="/opt/screenos"
# Raspberry Pi Imager ya no fuerza el usuario "pi" desde Raspberry Pi OS
# Bullseye en adelante -- se detecta el usuario real que invoco sudo (o quien
# es dueno de /home/pi si existe, como ultimo fallback) en vez de asumirlo.
SERVICE_USER="${SUDO_USER:-pi}"
LOG_FILE="/tmp/screenar-install.log"

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'

log()     { echo -e "${GREEN}[OK]${NC} $1" | tee -a "$LOG_FILE"; }
warn()    { echo -e "${YELLOW}[!]${NC} $1" | tee -a "$LOG_FILE"; }
error()   { echo -e "${RED}[X]${NC} $1" | tee -a "$LOG_FILE"; exit 1; }
section() { echo -e "\n${BLUE}--- $1 ---${NC}" | tee -a "$LOG_FILE"; }

while [[ "$#" -gt 0 ]]; do
  case $1 in
    --api) API_URL="$2"; shift ;;
    *) warn "Argumento desconocido: $1" ;;
  esac
  shift
done

echo -e "${BLUE}"
cat << 'EOF'
  ================================================
     ScreenAR -- Instalador de Raspberry Pi
     Digital Signage Player
  ================================================
EOF
echo -e "${NC}"

if [ "$EUID" -ne 0 ]; then
  error "Ejecuta con sudo: sudo bash install.sh"
fi

log "Usuario de servicio detectado: $SERVICE_USER"

if [ -z "$API_URL" ]; then
  echo -e "${YELLOW}Cual es la URL de tu servidor ScreenAR?${NC}"
  echo "Ejemplo: https://panel.screenar.app"
  read -p "URL: " API_URL
fi
if [ -z "$API_URL" ]; then
  error "La URL del servidor es obligatoria"
fi
API_URL="${API_URL%/}"
log "Servidor: $API_URL"

section "Actualizando sistema"
apt-get update -qq && apt-get upgrade -y -qq
log "Sistema actualizado"

section "Instalando dependencias"
apt-get install -y -qq \
  chromium \
  xorg \
  openbox \
  x11-xserver-utils \
  feh \
  scrot \
  unclutter \
  curl \
  jq \
  python3 \
  python3-gpiozero \
  python3-lgpio \
  avahi-daemon \
  network-manager \
  wmctrl 2>/dev/null || true
log "Dependencias instaladas"

section "Creando estructura"
mkdir -p "$INSTALL_DIR"/{scripts,logs,cache/media,assets}
chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR"
log "Directorios creados en $INSTALL_DIR"

section "Guardando configuracion"
cat > "$INSTALL_DIR/config.json" << EOF
{
  "api_url": "$API_URL",
  "screen_token": "",
  "version": "1.0.0",
  "installed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR/config.json"
log "Configuracion guardada"

section "Instalando splash de arranque"
base64 -d << '__SCREENAR_SPLASH_B64__' > "$INSTALL_DIR/assets/splash.png"
iVBORw0KGgoAAAANSUhEUgAAB4AAAAQ4CAYAAADo08FDAAAAAXNSR0IArs4c6QAAAARnQU1BAACx
jwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAFk7SURBVHhe7N0/yC3XmSfqm9/ghh1ccDCBggEF
A9eYwcaJNTBYke1ITmwUWHTgBoMwTUODfBF0Q8MJDG0wxhc7MCgQKLGcyIpEJ8KBmQkGYzDMMIGN
E9NwaWea+R1/W/p0zjrf+f5UvWvVW88LD7Ta0tm1q2rX3mf91nrX//F//dW//wgAAAAAAACA4xMA
AwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAA
gCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAA
AAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQh
AAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAA
AABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAA
AAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABo
QgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAA
AAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJg
AAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA
0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAA
AAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYE
wAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAA
AKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYA
AAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABN
CIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAA
AABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAM
AAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAA
mhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAA
AAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQA
GAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAA
ADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAA
AAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJ
ATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAA
AABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIAB
AAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABA
EwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAA
AAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAA
AwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAA
gCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAA
AAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQh
AAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAA
AABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAA
AAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABo
QgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAA
AAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJg
AAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA
0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAA
AAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYE
wAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAA
AKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYA
AAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABN
CIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAA
AABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAM
AAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAA
mhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAA
AAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQA
GAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAA
ADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAA
AAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJ
ATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAA
AABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIAB
AAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABA
EwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAA
AAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAA
AwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAA
gCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAA
AAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQh
AAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAA
AABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAA
AAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABo
QgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAA
AAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJg
AAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA
0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAA
AAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYE
wAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAA
AKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYA
AAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABN
CIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAA
AABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAM
AAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAA
mhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAA
AAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMAAAAAAAA0IQA
GAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAAAAAAAABNCIABAAAAAAAAmhAAAwAAAAAA
ADQhAAYAAAAAAABoQgAMAAAAAAAA0IQAGAAAAAAAAKAJATAAAAAAAABAEwJgAAAAAAAAgCYEwAAA
AAAAAABNCIABAAAAAAAAmhAAAwAAAAAAADQhAAYAAAAAAABoQgAMABzOy1/5xsde/H/+0/DfAQAA
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
BgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAA
AE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAA
AAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhC
AAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAA
AACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAA
AAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQ
hAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAA
AAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTA
AAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAA
oAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAA
AAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0I
gAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAA
AEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwA
AAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACa
EAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAA
AACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAY
AAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAA
NCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAA
AAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkB
MAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAA
AGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEA
AAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEAT
AmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAA
AADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAAD
AAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACA
JgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAA
AAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEA
BgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAA
AE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAA
AAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhC
AAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAA
AACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAA
AAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQ
hAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAA
AAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTA
AAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAA
oAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAA
AAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0I
gAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAA
AEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwA
AAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACa
EAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAA
AACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAY
AAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAAAAAAAE0IgAEAAAAAAACaEAADAAAAAAAA
NCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAAAAAAAAoAkBMAAAAAAAAEATAmAAAAAAAACAJgTAAAAA
AAAAAE0IgAEAAAAAAACaEAADAAAAAAAANCEABgAAAAAAAGhCAAwAAAAAAADQhAAYAOB/tWfHRAAA
MAyE/NS/v9THHwMqAAAAAACIEMAAAAAAAAAAEQIYAAAAAAAAIEIAAwAAAAAAAEQIYAAAAAAAAIAI
AQwAAAAAAAAQIYABAAAAAAAAIgQwAAAAAAAAQIQABgAAAAAAAIgQwAAAAAAAAAARAhgAAAAAAAAg
QgADAAAAAAAARAhgAAAAAAAAgAgBDAAAAAAAABAhgAEAAAAAAAAiBDAAAAAAAABAhAAGAAAAAAAA
iBDAAAAAAAAAABECGAAAAAAAACBCAAMAAAAAAABECGAAAAAAAACACAEMAAAAAAAAECGAAQAAAAAA
ACIEMAAAAAAAAECEAAYAAAAAAACIEMAAAAAAAAAAEQIYAAAAAAAAIEIAAwAAAAAAAEQIYAAAAAAA
AIAIAQwAAAAAAAAQIYABAAAAAAAAIgQwAAAAAAAAQIQABgAAAAAAAIgQwAAAAAAAAAARAhgAAAAA
AAAgQgADAAAAAAAARAhgAAAAAAAAgAgBDAAAAAAAABAhgAEAAAAAAAAiBDAAAAAAAABAhAAGAAAA
AAAAiBDAAAAAAAAAABECGAAAAAAAACBCAAMAAAAAAABECGAAAAAAAACACAEMAAAAAAAAECGAAQAA
AAAAACIEMAAAAAAAAECEAAYAAAAAAACIEMAAAAAAAAAAEQIYAAAAAAAAIEIAAwAAAAAAAEQIYAAA
AAAAAIAIAQwAAAAAAAAQIYABAAAAAAAAIgQwAAAAAAAAQIQABgAAAAAAAIgQwAAAAAAAAAARAhgA
AAAAAAAgQgADAAAAAAAARAhgAAAAAAAAgAgBDAAAAAAAABAhgAEAAAAAAAAiBDAAAAAAAABAhAAG
AAAAAAAAiBDAAAAAAAAAABECGAAAAAAAACBCAAMAAAAAAABECGAAAAAAAACACAEMAAAAAAAAECGA
AQAAAAAAACIEMAAAAAAAAECEAAYAAAAAAACIEMAAAAAAAAAAEQIYAAAAAAAAIEIAAwAAAAAAAEQI
YAAAAAAAAIAIAQwAAAAAAAAQIYABAAAAAAAAIgQwAAAAAAAAQIQABgAAAAAAAIgQwAAAAAAAAAAR
AhgAAAAAAAAgQgADAAAAAAAARAhgAAAAAAAAgAgBDAAAAAAAABAhgAEAAAAAAAAiBDAAAAAAAABA
hAAGAAAAAAAAiBDAAAAAAAAAABECGAAAAAAAACBCAAMAAAAAAABECGAAAAAAAACACAEMAAAAAAAA
ECGAAQAAAAAAACIEMAAAAAAAAECEAAYAAAAAAACIEMAAAAAAAAAAEQIYAAAAAAAAIEIAAwAAAAAA
AEQIYAAAAAAAAIAIAQwAAAAAAAAQIYABAAAAAAAAIgQwAAAAAAAAQIQABgAAAAAAAIgQwAAAAAAA
AAARAhgAAAAAAAAgQgADAAAAAAAARAhgAAAAAAAAgAgBDAAAAAAAABAhgAEAAAAAAAAiBDAAAAAA
AABAhAAGAAAAAAAAiBDAAAAAAAAAABECGAAAAAAAACBCAAMAAAAAAABECGAAAAAAAACACAEMAAAA
AAAAECGAAQAAAAAAABJuD8siPCuMrSOJAAAAAElFTkSuQmCC
__SCREENAR_SPLASH_B64__
chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR/assets/splash.png"
log "Splash instalado"

section "Instalando scripts del reproductor"
cat > "$INSTALL_DIR/scripts/cache-sync.py" << '__SCREENAR_SCRIPT_CACHE_SYNC_PY__'
#!/usr/bin/env python3
import json, os, time, urllib.request

CONFIG         = '/opt/screenos/config.json'
CACHE_DIR      = '/opt/screenos/cache/media'
SETUP_FILE     = '/opt/screenos/cache/setup.json'
LOCAL_PLAYLIST = '/opt/screenos/cache/playlist-local.json'
LOG            = '/opt/screenos/logs/cache.log'


def log(msg):
    os.makedirs(os.path.dirname(LOG), exist_ok=True)
    with open(LOG, 'a') as f:
        f.write('[%s] %s\n' % (time.strftime('%H:%M:%S'), msg))


def http_get_json(url, token=None, timeout=10):
    req = urllib.request.Request(url)
    if token:
        req.add_header('X-Screen-Token', token)
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read().decode())


def load_previous_zones():
    # Zonas que ya estaban cacheadas de una corrida anterior, indexadas por
    # numero de zona -- se usan como red de seguridad mas abajo: si el pedido
    # de una zona puntual falla (server caido a medias, timeout, etc.) pero
    # ANTES si habia contenido bueno guardado, no tiene sentido pisarlo con
    # una lista vacia solo porque este intento en particular no respondio.
    try:
        data = json.load(open(LOCAL_PLAYLIST))
        return {z.get('zone'): z for z in (data.get('zones') or [])}
    except Exception:
        return {}


def main():
    os.makedirs(CACHE_DIR, exist_ok=True)
    try:
        config = json.load(open(CONFIG))
    except Exception:
        return
    api_url = config.get('api_url')
    token = config.get('screen_token')
    if not api_url or not token:
        return

    try:
        setup = http_get_json(api_url + '/api/screens/player/setup', token=token, timeout=10)
    except Exception:
        log('Sin red')
        return

    # Mantener el setup cacheado fresco (save-setup.sh solo lo hace al bootear)
    try:
        json.dump(setup, open(SETUP_FILE, 'w'))
    except Exception:
        pass

    log('Sincronizando...')

    player_type = setup.get('player_type', 'standard')
    installation = setup.get('installation') or {}
    mode = installation.get('mode')

    # Modo independiente: una playlist por zona/pantalla. El resto (standard,
    # same, videowall-hardware) usa siempre la playlist programada única —
    # mismo criterio que ya sigue player.js en runVideowall().
    playlists = []
    if player_type == 'videowall' and mode == 'independent':
        for i, sc in enumerate(installation.get('screens') or []):
            playlists.append((i, sc.get('width'), sc.get('height'), sc.get('playlist_id')))
    else:
        playlists.append((0, None, None, None))

    previous_zones = load_previous_zones()
    zones_out = []
    seen_media = {}  # media_id -> nombre de archivo local (evita descargar 2 veces)

    for zone_idx, w, h, playlist_id in playlists:
        items = []
        fetch_failed = False
        try:
            if playlist_id:
                items = http_get_json(api_url + '/api/playlists/' + playlist_id + '/items-public', timeout=10)
            if not items:
                sched = http_get_json(api_url + '/api/screens/player/playlist', token=token, timeout=10)
                items = sched.get('items') or []
        except Exception:
            # Distinto de "el servidor respondió que no hay nada" — acá el
            # pedido en sí falló (timeout, servidor caído a medias). Antes
            # esto vaciaba items y terminaba pisando el caché bueno de la
            # corrida anterior con una lista vacía, dejando la pantalla en
            # negro aunque hubiera contenido perfectamente válido ya
            # descargado — mismo bug que se encontró y arregló el mismo día
            # en el cold-start de player.js (ver runStandard/fetchPlaylist).
            items = []
            fetch_failed = True

        if fetch_failed:
            prev = previous_zones.get(zone_idx)
            if prev and prev.get('items'):
                log('Zona %s: fallo el pedido, se mantiene el cache anterior (%d items)' % (zone_idx, len(prev['items'])))
                zones_out.append(prev)
                # Igual hay que registrar sus media_id como "vistos" para que
                # el borrado de archivos viejos de más abajo no los elimine.
                for it in prev['items']:
                    mid = it.get('media_id')
                    fname = (it.get('storage_url') or '').rsplit('/', 1)[-1]
                    if mid and fname:
                        seen_media[mid] = fname
                continue

        zone_items = []
        for item in items:
            if item.get('type') == 'html':
                continue  # los widgets son contenido vivo (reloj/clima/etc), no se cachean
            media_id = item.get('media_id')
            if not media_id:
                continue
            if media_id not in seen_media:
                url = item.get('storage_url', '')
                clean_url = url.split('?')[0]
                base = clean_url.rsplit('/', 1)[-1]
                ext = base.rsplit('.', 1)[-1] if '.' in base else 'bin'
                filename = media_id + '.' + ext
                filepath = os.path.join(CACHE_DIR, filename)
                if not os.path.isfile(filepath) or os.path.getsize(filepath) == 0:
                    log('Descargando: ' + str(item.get('media_name') or item.get('name') or media_id))
                    try:
                        urllib.request.urlretrieve(url, filepath)
                        ok = os.path.getsize(filepath) > 0
                    except Exception:
                        ok = False
                    if ok:
                        log('OK')
                    else:
                        log('Error')
                        if os.path.exists(filepath):
                            os.remove(filepath)
                        continue
                seen_media[media_id] = filename
            if media_id in seen_media:
                zone_items.append({
                    'media_id': media_id,
                    'type': item.get('type'),
                    'name': item.get('media_name') or item.get('name'),
                    'duration_sec': item.get('effective_duration_sec') or item.get('duration_sec') or 15,
                    'storage_url': '/media/' + seen_media[media_id],
                })

        zones_out.append({'zone': zone_idx, 'width': w, 'height': h, 'items': zone_items})

    # Borrar archivos cacheados que ya no pertenecen a ninguna playlist activa
    current_files = set(seen_media.values())
    for fname in os.listdir(CACHE_DIR):
        if fname not in current_files:
            log('Eliminando viejo: ' + fname)
            os.remove(os.path.join(CACHE_DIR, fname))

    try:
        json.dump({'mode': mode or 'standard', 'zones': zones_out}, open(LOCAL_PLAYLIST, 'w'))
    except Exception:
        pass

    log('Sync completo. %d archivos en cache.' % len(os.listdir(CACHE_DIR)))


if __name__ == '__main__':
    main()
__SCREENAR_SCRIPT_CACHE_SYNC_PY__
chmod 644 "$INSTALL_DIR/scripts/cache-sync.py"

cat > "$INSTALL_DIR/scripts/fileserver.py" << '__SCREENAR_SCRIPT_FILESERVER_PY__'
#!/usr/bin/env python3
# Servidor de archivos local para la caché de medios (/opt/screenos/cache) —
# reemplaza a `python3 -m http.server`, que NO soporta range requests (bytes=
# start-end). Confirmado con curl real contra un video de 422MB: devolvía
# siempre 200 + el archivo entero, sin Accept-Ranges ni 206 Partial Content
# (Python 3.13.5 en esta Pi). El navegador arma el <video> pidiendo rangos —
# sin esa respuesta no puede empezar a reproducir hasta bajar el archivo
# entero (con videos grandes, tarda tanto que en la práctica no arranca
# nunca). Mismo patrón de handler crudo que remote-control.py, sin
# dependencias nuevas (solo stdlib).

import os
import re
import mimetypes
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

CACHE_DIR = '/opt/screenos/cache'
PORT = 8888
RANGE_RE = re.compile(r'bytes=(\d*)-(\d*)')


class RangeFileHandler(BaseHTTPRequestHandler):
    server_version = 'ScreenOSFileServer/1.0'

    def log_message(self, *args):
        pass  # mismo criterio que remote-control.py: no ensuciar journalctl con cada request

    def _resolve_path(self):
        # Evita path traversal (../../etc) — el resultado tiene que quedar
        # adentro de CACHE_DIR después de resolver symlinks/".." .
        rel = self.path.split('?', 1)[0].lstrip('/')
        full = os.path.realpath(os.path.join(CACHE_DIR, rel))
        if not full.startswith(os.path.realpath(CACHE_DIR) + os.sep):
            return None
        return full

    def _send_file(self, method):
        path = self._resolve_path()
        if not path or not os.path.isfile(path):
            self.send_error(404, 'No encontrado')
            return

        file_size = os.path.getsize(path)
        content_type = mimetypes.guess_type(path)[0] or 'application/octet-stream'
        range_header = self.headers.get('Range')

        start, end = 0, file_size - 1
        status = 200
        if range_header:
            m = RANGE_RE.match(range_header)
            if m:
                status = 206
                if m.group(1):
                    start = int(m.group(1))
                    end = int(m.group(2)) if m.group(2) else file_size - 1
                else:
                    # formato "bytes=-N" -> los últimos N bytes
                    suffix = int(m.group(2))
                    start = max(0, file_size - suffix)
                    end = file_size - 1
                start = max(0, min(start, file_size - 1))
                end = max(start, min(end, file_size - 1))

        length = end - start + 1

        self.send_response(status)
        self.send_header('Content-Type', content_type)
        self.send_header('Accept-Ranges', 'bytes')
        self.send_header('Content-Length', str(length))
        if status == 206:
            self.send_header('Content-Range', f'bytes {start}-{end}/{file_size}')
        self.end_headers()

        if method == 'HEAD':
            return

        with open(path, 'rb') as f:
            f.seek(start)
            remaining = length
            chunk = 1024 * 256
            while remaining > 0:
                data = f.read(min(chunk, remaining))
                if not data:
                    break
                try:
                    self.wfile.write(data)
                except (BrokenPipeError, ConnectionResetError):
                    return  # el navegador cortó la conexión (cambió de item, etc.) -- normal, no es error
                remaining -= len(data)

    def do_GET(self):
        self._send_file('GET')

    def do_HEAD(self):
        self._send_file('HEAD')


if __name__ == '__main__':
    server = ThreadingHTTPServer(('0.0.0.0', PORT), RangeFileHandler)
    server.serve_forever()
__SCREENAR_SCRIPT_FILESERVER_PY__
chmod 755 "$INSTALL_DIR/scripts/fileserver.py"

cat > "$INSTALL_DIR/scripts/fileserver.sh" << '__SCREENAR_SCRIPT_FILESERVER_SH__'
#!/bin/bash
exec python3 /opt/screenos/scripts/fileserver.py
__SCREENAR_SCRIPT_FILESERVER_SH__
chmod 755 "$INSTALL_DIR/scripts/fileserver.sh"

cat > "$INSTALL_DIR/scripts/local-player-server.py" << '__SCREENAR_SCRIPT_LOCAL_PLAYER_SERVER_PY__'
import http.server, json, os, socketserver

CACHE_DIR    = '/opt/screenos/cache'
PLAYLIST     = CACHE_DIR + '/playlist-local.json'
MEDIA_DIR    = CACHE_DIR + '/media'
PORT         = 7777

HTML = """<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>ScreenOS</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100vw;height:100vh;overflow:hidden;background:#000}
.zone{position:absolute;overflow:hidden;background:#000}
.zone img,.zone video{width:100%;height:100%;object-fit:fill}
#info{position:fixed;bottom:8px;right:8px;color:rgba(255,255,255,0.3);font-family:sans-serif;font-size:11px;z-index:10}
#offline{position:fixed;top:8px;right:8px;background:rgba(239,68,68,0.2);color:#FCA5A5;font-family:sans-serif;font-size:11px;padding:4px 10px;border-radius:99px;font-weight:600;z-index:10}
</style></head>
<body>
<div id="info">ScreenOS — Modo offline</div>
<div id="offline">Sin red</div>
<script>
const zoneState = {}; // zoneId -> { items, idx, timer }

function tickZone(zoneId) {
  const z = zoneState[zoneId];
  const el = document.getElementById(zoneId);
  if (!z || !el || !z.items.length) return;

  clearTimeout(z.timer);
  const item = z.items[z.idx];
  el.innerHTML = '';

  if (item.type === 'video') {
    const v = document.createElement('video');
    v.autoplay = true; v.muted = true; v.playsInline = true;
    v.src = item.storage_url;
    el.appendChild(v);
  } else if (item.type === 'image') {
    const img = document.createElement('img');
    img.src = item.storage_url;
    el.appendChild(img);
  }

  const dur = (item.duration_sec || 15) * 1000;
  z.timer = setTimeout(() => {
    z.idx = (z.idx + 1) % z.items.length;
    tickZone(zoneId);
  }, dur);
}

function makeZoneEl(id, xPct, wPct, hPct) {
  const div = document.createElement('div');
  div.className = 'zone';
  div.id = id;
  div.style.left = xPct + '%';
  div.style.top = '0';
  div.style.width = wPct + '%';
  div.style.height = hPct + '%';
  document.body.appendChild(div);
  return div;
}

async function boot() {
  let data;
  try {
    const r = await fetch('/playlist.json');
    data = await r.json();
  } catch (e) {
    document.getElementById('info').textContent = 'Sin contenido en cache';
    return;
  }

  const zones = data.zones || [];
  const withContent = zones.filter(z => z.items && z.items.length);
  if (!withContent.length) {
    document.getElementById('info').textContent = 'Sin contenido en cache';
    return;
  }

  // Limpiar el estado y los elementos de la corrida anterior antes de
  // reconstruir -- boot() se vuelve a llamar cada 60s (setInterval mas abajo)
  // para levantar cambios de playlist sin recargar toda la pagina. Sin esto,
  // cada corrida agregaba un <div class="zone"> nuevo encima del anterior con
  // el mismo id -- getElementById siempre devuelve el primero, asi que el div
  // nuevo quedaba negro y vacio para siempre, tapando el contenido real
  // (bug real: la pantalla se ponia negra ~1 minuto despues de arrancar).
  for (const z of Object.values(zoneState)) clearTimeout(z.timer);
  for (const key of Object.keys(zoneState)) delete zoneState[key];
  document.querySelectorAll('.zone').forEach(el => el.remove());

  if (data.mode === 'independent' && zones.length > 1) {
    const physW = window.innerWidth, physH = window.innerHeight;
    let offsetX = 0;
    for (const z of zones) {
      const w = Number(z.width) || 0, h = Number(z.height) || 0;
      if (!w || !h) continue;
      const xPct = (offsetX / physW) * 100;
      const wPct = (w / physW) * 100;
      const hPct = (h / physH) * 100;
      const id = 'zone-' + z.zone;
      makeZoneEl(id, xPct, wPct, hPct);
      zoneState[id] = { items: z.items, idx: 0, timer: null };
      if (z.items.length) tickZone(id);
      offsetX += w;
    }
  } else {
    const z = withContent[0];
    const id = 'zone-' + z.zone;
    makeZoneEl(id, 0, 100, 100);
    zoneState[id] = { items: z.items, idx: 0, timer: null };
    tickZone(id);
  }
}

boot();
setInterval(boot, 60000);
</script>
</body></html>"""

class Handler(http.server.BaseHTTPRequestHandler):
    def log_message(self, *args): pass
    def do_GET(self):
        if self.path == '/' or self.path == '/index.html':
            body = HTML.encode()
            self.send_response(200)
            self.send_header('Content-Type', 'text/html')
            self.send_header('Content-Length', len(body))
            self.end_headers()
            self.wfile.write(body)
        elif self.path == '/playlist.json':
            try:
                with open(PLAYLIST, 'rb') as f: body = f.read()
                self.send_response(200)
                self.send_header('Content-Type', 'application/json')
                self.send_header('Content-Length', len(body))
                self.end_headers()
                self.wfile.write(body)
            except Exception:
                self.send_response(404); self.end_headers()
        elif self.path.startswith('/media/'):
            filepath = CACHE_DIR + self.path
            if os.path.exists(filepath):
                ext = filepath.split('.')[-1].lower()
                ct = 'video/mp4' if ext in ['mp4', 'webm'] else 'image/jpeg'
                with open(filepath, 'rb') as f: body = f.read()
                self.send_response(200)
                self.send_header('Content-Type', ct)
                self.send_header('Content-Length', len(body))
                self.send_header('Cross-Origin-Resource-Policy', 'cross-origin')
                self.end_headers()
                self.wfile.write(body)
            else:
                self.send_response(404); self.end_headers()
        else:
            self.send_response(404); self.end_headers()

print(f'[LocalPlayer] Corriendo en puerto {PORT}')
socketserver.TCPServer(('0.0.0.0', PORT), Handler).serve_forever()
__SCREENAR_SCRIPT_LOCAL_PLAYER_SERVER_PY__
chmod 644 "$INSTALL_DIR/scripts/local-player-server.py"

cat > "$INSTALL_DIR/scripts/network-watchdog.sh" << '__SCREENAR_SCRIPT_NETWORK_WATCHDOG_SH__'
#!/bin/bash
CONFIG="/opt/screenos/config.json"
API_URL=$(jq -r '.api_url' "$CONFIG" 2>/dev/null)
WAS_OFFLINE=false

while true; do
  if curl -s --connect-timeout 5 "$API_URL/health" > /dev/null 2>&1; then
    if [ "$WAS_OFFLINE" = true ]; then
      echo "[Watchdog] Red recuperada - reiniciando player"
      pkill chromium
      sleep 3
      DISPLAY=:0 /bin/bash /opt/screenos/scripts/start-player.sh &
      WAS_OFFLINE=false
    fi
  else
    if [ "$WAS_OFFLINE" = false ]; then
      echo "[Watchdog] Se perdio la red - reiniciando player en modo cache local"
      pkill chromium
      sleep 3
      DISPLAY=:0 /bin/bash /opt/screenos/scripts/start-player.sh &
    fi
    WAS_OFFLINE=true
    echo "[Watchdog] Sin red"
  fi
  sleep 15
done
__SCREENAR_SCRIPT_NETWORK_WATCHDOG_SH__
chmod 755 "$INSTALL_DIR/scripts/network-watchdog.sh"

cat > "$INSTALL_DIR/scripts/remote-control.py" << '__SCREENAR_SCRIPT_REMOTE_CONTROL_PY__'
#!/usr/bin/env python3
import http.server, json, os, subprocess, socketserver, time, urllib.request, threading

PORT = 9999
CONFIG_PATH = '/opt/screenos/config.json'
POLL_INTERVAL_SEC = 7

def read_cpu_times():
    with open('/proc/stat') as f:
        parts = f.readline().split()[1:]
    nums = [int(x) for x in parts]
    idle = nums[3] + nums[4]
    total = sum(nums)
    return idle, total

def cpu_pct():
    try:
        idle1, total1 = read_cpu_times()
        time.sleep(0.3)
        idle2, total2 = read_cpu_times()
        d_idle = idle2 - idle1
        d_total = total2 - total1
        if d_total <= 0:
            return None
        return round((1 - d_idle / d_total) * 100, 1)
    except Exception:
        return None

def cpu_temp():
    try:
        out = subprocess.check_output(['vcgencmd', 'measure_temp'], timeout=3).decode()
        return float(out.strip().replace('temp=', '').replace("'C", ''))
    except Exception:
        return None

def gpu_mem_mb():
    try:
        out = subprocess.check_output(['vcgencmd', 'get_mem', 'gpu'], timeout=3).decode()
        return int(out.strip().replace('gpu=', '').replace('M', ''))
    except Exception:
        return None

def throttled_flags():
    try:
        out = subprocess.check_output(['vcgencmd', 'get_throttled'], timeout=3).decode()
        val = int(out.strip().replace('throttled=', ''), 16)
        return {
            'under_voltage_now':      bool(val & 0x1),
            'freq_capped_now':        bool(val & 0x2),
            'throttled_now':          bool(val & 0x4),
            'under_voltage_occurred': bool(val & 0x10000),
            'freq_capped_occurred':   bool(val & 0x20000),
            'throttled_occurred':     bool(val & 0x40000),
        }
    except Exception:
        return None

def ram_info():
    try:
        info = {}
        with open('/proc/meminfo') as f:
            for line in f:
                key, val = line.split(':')
                info[key.strip()] = int(val.strip().split()[0])  # kB
        total = info.get('MemTotal', 0)
        available = info.get('MemAvailable', 0)
        used = total - available
        return {
            'ram_total_mb': round(total / 1024),
            'ram_used_mb': round(used / 1024),
            'ram_pct': round(used / total * 100, 1) if total else None,
        }
    except Exception:
        return {'ram_total_mb': None, 'ram_used_mb': None, 'ram_pct': None}

def load_config():
    try:
        return json.load(open(CONFIG_PATH))
    except Exception:
        return {}


# ── Acciones remotas — misma lógica de siempre. ANTES vivían adentro de
# do_POST, disparadas por un POST entrante al puerto 9999 (solo funcionaba en
# la misma LAN); AHORA las llama poll_loop() más abajo, disparadas por un
# comando que la propia Pi fue a buscar — cero cambios en QUÉ hace cada
# acción, solo en QUIÉN la dispara. Devuelven el dict de resultado en vez de
# escribirlo directo a una respuesta HTTP (no hay ninguna conexión HTTP
# entrante que responder acá).

def action_status():
    import shutil
    total, used, free = shutil.disk_usage('/opt/screenos/cache')
    cache_files = os.listdir('/opt/screenos/cache/media')
    cached_media_ids = sorted({f.rsplit('.', 1)[0] for f in cache_files})

    cache_total = 0
    cache_cached = 0
    try:
        # cache-sync.py escribe playlist-local.json (no "playlist.json" —
        # desacople de nombres entre los dos scripts, encontrado 2026-07-27:
        # esto hacía que cache_total/cache_cached/cache_pct fallaran
        # silenciosamente siempre, aunque cached_media_ids ya era correcto).
        local = json.load(open('/opt/screenos/cache/playlist-local.json'))
        items = [it for zone in local.get('zones', []) for it in zone.get('items', [])]
        cache_total = len(items)
        cache_cached = sum(1 for it in items if it.get('media_id') in cached_media_ids)
    except Exception:
        pass

    return {
        'ok': True,
        'disk_total': total,
        'disk_used': used,
        'disk_free': free,
        'cache_files': len(cache_files),
        'cache_total': cache_total,
        'cache_cached': cache_cached,
        'cache_pct': round(cache_cached / cache_total * 100) if cache_total else None,
        'cached_media_ids': cached_media_ids,
        'cpu_pct': cpu_pct(),
        'cpu_temp': cpu_temp(),
        'gpu_mem_mb': gpu_mem_mb(),
        'throttled': throttled_flags(),
        **ram_info(),
    }

def action_screenshot():
    import base64, tempfile
    tmp_path = tempfile.mktemp(suffix='.png')
    try:
        env = dict(os.environ, DISPLAY=':0')
        subprocess.run(['scrot', '-o', tmp_path], env=env, check=True, timeout=10)
        with open(tmp_path, 'rb') as f:
            img_b64 = base64.b64encode(f.read()).decode('ascii')
        return {'ok': True, 'image': img_b64}
    except Exception as e:
        return {'ok': False, 'error': str(e)}
    finally:
        if os.path.exists(tmp_path):
            os.remove(tmp_path)

def action_restart_player():
    subprocess.Popen(['pkill', 'chromium'])
    return {'ok': True}

def action_reboot():
    subprocess.Popen(['sudo', 'reboot'])
    return {'ok': True}

# Distinto de reboot: apaga el sistema operativo del todo (no vuelve a
# prender solo) -- recien despues de esto es seguro cortarle la luz a la Pi
# sin arriesgar la tarjeta SD (ver ScreenAR Mobile fase 1, boton "Apagar
# equipo" en Dispositivos.jsx). Igual que reboot, no hay garantia de que el
# POST a command-result de poll_loop() llegue a tiempo antes de que se corte
# la red -- el frontend ya no espera una confirmacion de "listo" para esta
# accion, solo avisa un tiempo prudencial antes de que sea seguro desenchufar.
def action_shutdown():
    subprocess.Popen(['sudo', 'shutdown', '-h', 'now'])
    return {'ok': True}

ACTIONS = {
    'status': action_status,
    'screenshot': action_screenshot,
    'restart_player': action_restart_player,
    'reboot': action_reboot,
    'shutdown': action_shutdown,
}


# ── Polling de comandos (Camino B) — reemplaza al viejo do_POST de acciones.
# Cada POLL_INTERVAL_SEC le pregunta al backend "¿tenés algo para mí?" (mismo
# sentido que el heartbeat de player.js, que ya funciona sin abrir ningún
# puerto) y reporta el resultado — funciona aunque esta Pi esté en una red
# distinta a la del servidor, sin VPN ni port-forwarding.

def poll_loop():
    while True:
        config = load_config()
        token = config.get('screen_token')
        api_url = config.get('api_url')
        if not token or not api_url:
            time.sleep(POLL_INTERVAL_SEC)
            continue

        try:
            req = urllib.request.Request(
                api_url + '/api/screens/player/pending-commands',
                headers={'X-Screen-Token': token},
            )
            with urllib.request.urlopen(req, timeout=10) as r:
                data = json.loads(r.read().decode())
        except Exception as e:
            print('[RemoteControl] error consultando comandos pendientes:', e)
            time.sleep(POLL_INTERVAL_SEC)
            continue

        command = data.get('command')
        if command:
            action = command.get('action')
            command_id = command.get('id')
            handler = ACTIONS.get(action)
            print('[RemoteControl] ejecutando comando', command_id, action)
            try:
                result = handler() if handler else {'ok': False, 'error': 'accion desconocida: ' + str(action)}
            except Exception as e:
                result = {'ok': False, 'error': str(e)}

            try:
                body = json.dumps({'command_id': command_id, 'ok': result.get('ok', False), 'result': result}).encode()
                req2 = urllib.request.Request(
                    api_url + '/api/screens/player/command-result',
                    data=body,
                    headers={'Content-Type': 'application/json', 'X-Screen-Token': token},
                    method='POST',
                )
                urllib.request.urlopen(req2, timeout=10)
            except Exception as e:
                print('[RemoteControl] error reportando resultado:', e)

        time.sleep(POLL_INTERVAL_SEC)


# ── Portal de activación local — SIN CAMBIOS respecto a la versión anterior,
# no tiene nada que ver con el canal de comandos remotos de arriba. Sigue
# siendo un paso local (instalador físicamente en la misma red que la Pi).

ACTIVATION_PAGE = """<!DOCTYPE html>
<html lang="es"><head><meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ScreenOS -- Activar pantalla</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  body { font-family:-apple-system,'Segoe UI',sans-serif; background:#0F172A; color:#fff;
    min-height:100vh; display:flex; align-items:center; justify-content:center; padding:16px; }
  .card { background:#1E293B; border-radius:24px; padding:32px 24px; width:100%; max-width:420px; border:1px solid #334155; }
  h1 { font-size:20px; font-weight:700; margin-bottom:6px; }
  p.subtitle { font-size:13px; color:#64748B; margin-bottom:24px; line-height:1.5; }
  label { display:block; font-size:12px; font-weight:600; color:#94A3B8; margin-bottom:6px; text-transform:uppercase; letter-spacing:0.05em; }
  input { width:100%; background:#0F172A; border:1.5px solid #334155; border-radius:12px; padding:14px 16px;
    color:#fff; font-size:15px; margin-bottom:16px; outline:none; }
  input:focus { border-color:#F59E0B; }
  .btn { width:100%; background:#F59E0B; color:#000; border:none; border-radius:12px; padding:15px;
    font-size:15px; font-weight:700; cursor:pointer; }
  .btn:disabled { background:#334155; color:#64748B; }
  .msg { border-radius:10px; padding:12px; font-size:13px; margin-top:16px; display:none; }
  .msg.error { background:rgba(239,68,68,0.1); border:1px solid rgba(239,68,68,0.3); color:#FCA5A5; }
  .msg.ok { background:rgba(34,197,94,0.1); border:1px solid rgba(34,197,94,0.3); color:#86EFAC; }
</style></head>
<body>
<div class="card">
  <h1>Activar esta pantalla</h1>
  <p class="subtitle">Pega el codigo de activacion que te dio tu proveedor.</p>
  <label>Codigo de activacion</label>
  <input type="text" id="token" placeholder="Pega el codigo aqui" autocomplete="off" autocorrect="off" spellcheck="false">
  <button class="btn" id="btn" onclick="activate()">Activar</button>
  <div class="msg error" id="err"></div>
  <div class="msg ok" id="ok"></div>
</div>
<script>
function activate() {
  var token = document.getElementById('token').value.trim();
  if (!token) return;
  var btn = document.getElementById('btn');
  var err = document.getElementById('err');
  var ok = document.getElementById('ok');
  err.style.display = 'none'; ok.style.display = 'none';
  btn.disabled = true; btn.textContent = 'Activando...';
  fetch('/activate-token', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({token: token})
  }).then(function (r) { return r.json().then(function (d) { return {status: r.status, body: d}; }); })
    .then(function (res) {
      if (res.status === 200 && res.body.ok) {
        ok.textContent = 'Pantalla activada (' + (res.body.screen_name || '') + '). Reiniciando el reproductor...';
        ok.style.display = 'block';
        btn.textContent = 'Listo';
      } else {
        err.textContent = res.body.error || 'No se pudo activar. Revisa el codigo.';
        err.style.display = 'block';
        btn.disabled = false; btn.textContent = 'Activar';
      }
    })
    .catch(function () {
      err.textContent = 'Error de conexion.';
      err.style.display = 'block';
      btn.disabled = false; btn.textContent = 'Activar';
    });
}
</script>
</body></html>"""

ALREADY_ACTIVE_PAGE = """<!DOCTYPE html>
<html lang="es"><head><meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ScreenOS</title>
<style>
  body { font-family:-apple-system,'Segoe UI',sans-serif; background:#0F172A; color:#fff;
    min-height:100vh; display:flex; align-items:center; justify-content:center; padding:16px; text-align:center; }
</style></head>
<body><div><h2>Esta pantalla ya esta activada.</h2>
<p style="color:#64748B;margin-top:8px">Si necesitas re-activarla con otro codigo, borra manualmente screen_token en config.json.</p>
</div></body></html>"""

class Handler(http.server.BaseHTTPRequestHandler):
    def log_message(self, *args): pass

    # El navegador de la pantalla llama a /activate-token desde otro origen
    # (la pagina de pareo la sirve el backend central en :3000, este server
    # vive en :9999 del mismo dispositivo) - eso dispara un preflight CORS
    # (OPTIONS) por el Content-Type: application/json. Sin esto, el navegador
    # bloquea el POST antes de mandarlo (confirmado: el pareo automatico
    # funcionaba del lado del backend pero nunca llegaba a escribir
    # config.json en el dispositivo).
    def do_OPTIONS(self):
        self.send_response(204)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        self.end_headers()

    def do_GET(self):
        config = load_config()
        if config.get('screen_token'):
            page = ALREADY_ACTIVE_PAGE
        else:
            page = ACTIVATION_PAGE
        body = page.encode('utf-8')
        self.send_response(200)
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        self.send_header('Content-Length', str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    # Ya NO maneja comandos remotos entrantes (reboot/restart_player/status/
    # screenshot) — eso ahora lo dispara poll_loop(). Lo único que queda acá
    # es /activate-token (paso local de una sola vez).
    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        body = json.loads(self.rfile.read(length)) if length else {}

        if self.path == '/activate-token':
            return self.handle_activate(body)

        self.send_response(404)
        self.end_headers()

    # Antes esto se negaba (403) si config.json ya tenia un screen_token
    # guardado -- solo servia para la primera activacion. Si el token de
    # esta pantalla cambiaba despues (regenerar token, re-parear, restaurar
    # una pantalla borrada), este proceso quedaba con el token viejo para
    # siempre y dejaba de recibir comandos remotos EN SILENCIO, sin ningun
    # indicio (bug real encontrado 2026-08-08: heartbeat del navegador
    # online, pero ningun comando remoto llegaba nunca, ni siquiera
    # "status"). player.js ahora llama aca cada 2 minutos (no solo al
    # parear) con el token que el navegador esta usando de verdad, para
    # autocorregirse solo. Seguro sacar el candado: el token igual se
    # revalida contra /setup/validate-token antes de aceptarlo, esa
    # validacion siempre fue la proteccion real, no el "una sola vez".
    def handle_activate(self, body):
        config = load_config()
        token = (body.get('token') or '').strip()
        if not token:
            return self.json_response(400, {'ok': False, 'error': 'Codigo requerido.'})

        # Mismo token que ya tenemos guardado: nada que hacer. Confirmacion
        # silenciosa, sin revalidar contra el backend ni reiniciar Chromium
        # -- es el caso normal en cada sondeo periodico de los 2 minutos.
        if config.get('screen_token') == token:
            return self.json_response(200, {'ok': True, 'unchanged': True})

        had_token_before = bool(config.get('screen_token'))

        api_url = config.get('api_url')
        if not api_url:
            return self.json_response(500, {'ok': False, 'error': 'Falta api_url en config.json -- contacta al proveedor.'})

        try:
            req = urllib.request.Request(
                api_url + '/setup/validate-token',
                data=json.dumps({'token': token}).encode(),
                headers={'Content-Type': 'application/json'},
                method='POST',
            )
            with urllib.request.urlopen(req, timeout=8) as r:
                result = json.loads(r.read().decode())
        except Exception as e:
            return self.json_response(502, {'ok': False, 'error': 'No se pudo validar con el servidor: ' + str(e)})

        if not result.get('valid'):
            return self.json_response(401, {'ok': False, 'error': 'Codigo invalido.'})

        config['screen_token'] = token
        json.dump(config, open(CONFIG_PATH, 'w'), indent=2)

        self.json_response(200, {'ok': True, 'screen_name': result.get('screen_name')})
        # Reiniciar Chromium solo tiene sentido en la primera activacion de
        # verdad (estaba mostrando el cartel de codigo de pareo, hace falta
        # relanzarlo para que cargue la URL real). Si ya habia un token
        # guardado y esto es una autocorreccion de un desincronismo, el
        # navegador YA esta mostrando lo correcto (por eso su heartbeat
        # andaba bien) -- reiniciarlo aca seria una interrupcion visual
        # innecesaria en un cartel que ya estaba funcionando bien.
        if not had_token_before:
            subprocess.Popen(['pkill', 'chromium'])

    def json_response(self, status, payload):
        body = json.dumps(payload).encode()
        self.send_response(status)
        self.send_header('Content-Type', 'application/json')
        self.send_header('Content-Length', str(len(body)))
        self.send_header('Access-Control-Allow-Origin', '*')
        self.end_headers()
        self.wfile.write(body)

if __name__ == '__main__':
    threading.Thread(target=poll_loop, daemon=True).start()
    print('[RemoteControl] Activacion en puerto ' + str(PORT) + ' -- polling de comandos cada ' + str(POLL_INTERVAL_SEC) + 's')
    socketserver.TCPServer(('0.0.0.0', PORT), Handler).serve_forever()
__SCREENAR_SCRIPT_REMOTE_CONTROL_PY__
chmod 755 "$INSTALL_DIR/scripts/remote-control.py"

cat > "$INSTALL_DIR/scripts/reset-button.py" << '__SCREENAR_SCRIPT_RESET_BUTTON_PY__'
#!/usr/bin/env python3
# Boton fisico de reset de pareo — conectado a GPIO17 (pin fisico 11) y GND
# (pin fisico 9). Mantenido apretado 7 segundos, vacia screen_token en
# config.json Y reinicia el player (pkill chromium — el watchdog ya existente
# lo relanza solo, leyendo el config.json ya vacio) para que la pantalla
# muestre el codigo de pareo nuevo al instante, sin depender de que alguien
# la reinicie a mano por separado. Corre siempre en segundo plano via
# systemd, independiente de si Chromium/la pantalla estan funcionando.
import json
import subprocess
from gpiozero import Device, Button
from gpiozero.pins.lgpio import LGPIOFactory
from signal import pause

# Se fuerza el factory de pines a lgpio en vez de dejar que gpiozero lo
# autodetecte — bajo systemd (a diferencia de una sesion SSH interactiva) la
# autodeteccion caia al fallback viejo "native" (sysfs /sys/class/gpio), que
# esta bloqueado en el kernel de este Raspberry Pi OS y tira OSError.
Device.pin_factory = LGPIOFactory()

CONFIG_PATH = '/opt/screenos/config.json'
PIN = 17
HOLD_SECONDS = 7


def load_config():
    try:
        return json.load(open(CONFIG_PATH))
    except Exception:
        return {}


def reset_pairing():
    config = load_config()
    if not config.get('screen_token'):
        print('Ya estaba sin token, nada que hacer.')
        return
    config['screen_token'] = ''
    json.dump(config, open(CONFIG_PATH, 'w'), indent=2)
    print('Pareo reseteado: screen_token vaciado.')
    try:
        subprocess.Popen(['pkill', 'chromium'])
        print('Player reiniciado — el watchdog lo relanza solo, ya sin token, mostrando el codigo de pareo nuevo.')
    except Exception as e:
        print('No se pudo reiniciar el player automaticamente: ' + str(e) + ' — entra a http://<ip>:9999/ para activar de nuevo.')


button = Button(PIN, pull_up=True, hold_time=HOLD_SECONDS)
button.when_held = reset_pairing

print('Escuchando boton de reset en GPIO' + str(PIN) + ' (mantener ' + str(HOLD_SECONDS) + 's)...')
pause()
__SCREENAR_SCRIPT_RESET_BUTTON_PY__
chmod 755 "$INSTALL_DIR/scripts/reset-button.py"

cat > "$INSTALL_DIR/scripts/save-setup.sh" << '__SCREENAR_SCRIPT_SAVE_SETUP_SH__'
#!/bin/bash
CONFIG="/opt/screenos/config.json"
API_URL=$(jq -r '.api_url' "$CONFIG" 2>/dev/null)
TOKEN=$(jq -r '.screen_token' "$CONFIG" 2>/dev/null)
SETUP_CACHE="/opt/screenos/cache/setup.json"

SETUP=$(curl -s --connect-timeout 10 \
  "$API_URL/api/screens/player/setup" \
  -H "X-Screen-Token: $TOKEN" 2>/dev/null)

if [ -n "$SETUP" ] && echo "$SETUP" | jq -e '.player_type' > /dev/null 2>&1; then
  echo "$SETUP" > "$SETUP_CACHE"
  echo "[Setup] Config guardada: $(echo $SETUP | jq -r '.player_type')"
fi
__SCREENAR_SCRIPT_SAVE_SETUP_SH__
chmod 755 "$INSTALL_DIR/scripts/save-setup.sh"

cat > "$INSTALL_DIR/scripts/start-player.sh" << '__SCREENAR_SCRIPT_START_PLAYER_SH__'
#!/bin/bash
CONFIG="/opt/screenos/config.json"
API_URL=$(jq -r '.api_url' "$CONFIG" 2>/dev/null)
TOKEN=$(jq -r '.screen_token' "$CONFIG" 2>/dev/null)
SETUP_CACHE="/opt/screenos/cache/setup.json"

# Verificar internet y obtener setup
if curl -s --connect-timeout 5 "$API_URL/health" > /dev/null 2>&1; then
  echo "[Player] Con internet"
  # Guardar setup en caché para próximo arranque
  /bin/bash /opt/screenos/scripts/save-setup.sh
  SCREEN_IP=$(hostname -I | awk '{print $1}')
  PUBLIC_IP=$(curl -s --connect-timeout 3 https://api.ipify.org 2>/dev/null)
  URL="$API_URL/player?token=$TOKEN&screen_ip=$SCREEN_IP&public_ip=$PUBLIC_IP"
else
  echo "[Player] Sin internet - usando caché local"
  URL="http://127.0.0.1:7777"
  if ! curl -s http://127.0.0.1:7777 > /dev/null 2>&1; then
    python3 /opt/screenos/scripts/local-player-server.py &
    sleep 3
  fi
fi

# Leer resolución desde caché de setup
PLAYER_TYPE="standard"
TOTAL_W=1920
TOTAL_H=1080

if [ -f "$SETUP_CACHE" ]; then
  PLAYER_TYPE=$(jq -r '.player_type // "standard"' "$SETUP_CACHE")
  if [ "$PLAYER_TYPE" = "videowall" ]; then
    TOTAL_W=$(jq -r '.installation.total_width // 1920' "$SETUP_CACHE")
    TOTAL_H=$(jq -r '.installation.total_height // 1080' "$SETUP_CACHE")
  fi
fi

echo "[Player] Tipo: $PLAYER_TYPE — Resolución: ${TOTAL_W}x${TOTAL_H}"

# Saca el splash de arranque (ver .config/openbox/autostart) justo antes de
# que Chromium tome la pantalla — si no se mata acá se queda tapando todo.
pkill feh 2>/dev/null

chromium \
  --kiosk \
  --noerrdialogs \
  --disable-infobars \
  --no-first-run \
  --autoplay-policy=no-user-gesture-required \
  --disable-session-crashed-bubble \
  --disable-gpu \
  --disable-software-rasterizer \
  --no-sandbox \
  --disable-features=Translate,TranslateUI \
  --lang=es \
  --window-position=0,0 \
  "$URL"
__SCREENAR_SCRIPT_START_PLAYER_SH__
chmod 755 "$INSTALL_DIR/scripts/start-player.sh"

cat > "$INSTALL_DIR/scripts/watchdog.sh" << '__SCREENAR_SCRIPT_WATCHDOG_SH__'
#!/bin/bash
LAST_RES=""
while true; do
  sleep 15

  DISPLAY=:0 xrandr --auto >/dev/null 2>&1
  RES=$(DISPLAY=:0 xrandr 2>/dev/null | grep '^Screen 0' | grep -oE 'current [0-9]+ x [0-9]+')

  if [ -n "$LAST_RES" ] && [ "$RES" != "$LAST_RES" ]; then
    echo "[Watchdog] Resolución cambió ($LAST_RES -> $RES), reiniciando player..."
    pkill -x chromium
    sleep 2
    DISPLAY=:0 /bin/bash /opt/screenos/scripts/start-player.sh &
  elif ! pgrep -x "chromium" > /dev/null; then
    echo "[Watchdog] Chromium no está corriendo, reiniciando..."
    DISPLAY=:0 /bin/bash /opt/screenos/scripts/start-player.sh &
  fi

  LAST_RES="$RES"
done
__SCREENAR_SCRIPT_WATCHDOG_SH__
chmod 755 "$INSTALL_DIR/scripts/watchdog.sh"

chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR/scripts"
log "Scripts instalados"

section "Configurando X server"
cat > /etc/X11/Xwrapper.config << 'EOF'
allowed_users=anybody
needs_root_rights=yes
EOF
log "X server configurado"

section "Configurando reproductor (autostart)"
AUTOSTART_DIR="/home/$SERVICE_USER/.config/openbox"
mkdir -p "$AUTOSTART_DIR"
cat > "$AUTOSTART_DIR/autostart" << 'EOF'
xset s off &
xset s noblank &
xset -dpms &
unclutter -idle 1 -root &

xsetroot -solid '#0F172A'
feh --fullscreen --hide-pointer /opt/screenos/assets/splash.png &

sleep 5
/bin/bash /opt/screenos/scripts/start-player.sh
EOF
chown -R "$SERVICE_USER:$SERVICE_USER" "$AUTOSTART_DIR"
log "Autostart configurado"

section "Configurando inicio automatico"
BASHRC="/home/$SERVICE_USER/.bashrc"
if ! grep -q "screenar-startx" "$BASHRC" 2>/dev/null; then
  cat >> "$BASHRC" << 'EOF'

# ScreenAR -- Iniciar entorno grafico automaticamente
# screenar-startx
[[ -z $DISPLAY && $XDG_VTNR -eq 1 ]] && startx /usr/bin/openbox-session
EOF
fi
raspi-config nonint do_boot_behaviour B2 2>/dev/null || true
log "Autologin configurado"

section "Configurando servicios systemd"
cat > /etc/systemd/system/screenos-cache.service << EOF
[Unit]
Description=ScreenOS Cache Sync
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=$SERVICE_USER
ExecStart=/bin/bash -c 'while true; do /usr/bin/python3 $INSTALL_DIR/scripts/cache-sync.py; sleep 300; done'
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target
EOF
systemctl enable screenos-cache 2>/dev/null || true

cat > /etc/systemd/system/screenos-fileserver.service << EOF
[Unit]
Description=ScreenOS Local File Server
After=network.target

[Service]
Type=simple
User=$SERVICE_USER
ExecStart=/bin/bash $INSTALL_DIR/scripts/fileserver.sh
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF
systemctl enable screenos-fileserver 2>/dev/null || true

cat > /etc/systemd/system/screenos-network-watchdog.service << EOF
[Unit]
Description=ScreenOS Network Watchdog
After=network.target

[Service]
Type=simple
User=$SERVICE_USER
ExecStart=/bin/bash $INSTALL_DIR/scripts/network-watchdog.sh
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF
systemctl enable screenos-network-watchdog 2>/dev/null || true

cat > /etc/systemd/system/screenos-remote.service << EOF
[Unit]
Description=ScreenOS Remote Control
After=network.target

[Service]
Type=simple
User=$SERVICE_USER
ExecStart=/usr/bin/python3 $INSTALL_DIR/scripts/remote-control.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
systemctl enable screenos-remote 2>/dev/null || true

cat > /etc/systemd/system/screenos-reset-button.service << EOF
[Unit]
Description=Boton fisico de reset de pareo (GPIO17, mantener 7s)
After=multi-user.target

[Service]
WorkingDirectory=/home/$SERVICE_USER
Environment=PYTHONUNBUFFERED=1
ExecStart=/usr/bin/python3 $INSTALL_DIR/scripts/reset-button.py
Restart=always
RestartSec=3
User=$SERVICE_USER

[Install]
WantedBy=multi-user.target
EOF
systemctl enable screenos-reset-button 2>/dev/null || true

cat > /etc/systemd/system/screenos-watchdog.service << EOF
[Unit]
Description=ScreenOS Watchdog -- Reinicia el player si se cuelga
After=multi-user.target

[Service]
Type=simple
User=$SERVICE_USER
ExecStart=/bin/bash $INSTALL_DIR/scripts/watchdog.sh
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target
EOF
systemctl enable screenos-watchdog 2>/dev/null || true

systemctl daemon-reload
log "Servicios systemd configurados: cache, fileserver, network-watchdog, remote, reset-button, watchdog"

section "Listo!"
echo ""
echo -e "${GREEN}ScreenAR instalado correctamente.${NC}"
echo ""
echo "Proximos pasos:"
echo "  1. Reinicia la Pi: sudo reboot"
echo "  2. Al arrancar, la pantalla va a mostrar un codigo de 6 caracteres"
echo "  3. Desde el panel (Pantallas > Vincular pantalla), ingresa ese codigo"
echo ""
echo -e "${YELLOW}Queres reiniciar ahora? (s/n)${NC}"
read -r resp
if [[ "$resp" =~ ^[Ss]$ ]]; then
  reboot
fi
