#!/bin/bash
# VAC — Vitalinux Autoregistration Client
#
# Servicio systemd que registra este equipo en VAS y mantiene una copia
# local del inventario de red en /var/lib/vac/clients.json.
#
# Flujo:
#   Arranque: espera a VAS_HOST, registro inicial bloqueante, identity.json.
#
#   Cada ciclo:
#     1. Recopila hostname/IP/MAC y extras (según EXTRAS_ENABLED y EXEC_*).
#     2. Selfcheck local: compara con identity.json.
#        - Sin cambios → POST /heartbeat (ligero, solo last_seen).
#          Si 404 o error de red → POST /register (re-registro).
#        - Con cambios → POST /register; guarda identity.json.
#     3. GET /version → compara con versión local.
#     4. Si nueva versión:
#        - Si SYNC_CLIENTS=true: GET /clients → clients.json.
#        - GET /clients/{UUID} → identity.json (reflejo desde VAS).
#        - Actualizar versión local.
#
# Journalctl: journalctl -u vac -f

set -euo pipefail

LOG_TAG="[VAC]"
source /usr/lib/vac/vac-common.sh

# ---------------------------------------------------------------------------
# Inicialización
# ---------------------------------------------------------------------------
load_all_conf

mkdir -p "$STATE_DIR"

if [[ ! -f "$ID_FILE" ]]; then
    uuidgen > "$ID_FILE"
    chmod 600 "$ID_FILE"
    log "UUID generado: $(cat "$ID_FILE")"
fi

if [[ ! -f "$VERSION_FILE" ]]; then
    echo "0" > "$VERSION_FILE"
    log "Fichero de versión inicializado: $VERSION_FILE → 0"
fi

CLIENT_ID="$(tr -d '[:space:]' < "$ID_FILE")"

log "=== Iniciando VAC ==="
log "UUID:           $CLIENT_ID"
log "VAS_HOST:       ${VAS_HOST:-'(no definido)'}"
log "CHECK:          ${CHECK_SECONDS}s"
log "RETRY:          ${RETRY_SECONDS}s"
log "EXTRAS_ENABLED: $EXTRAS_ENABLED"
log "EXEC_IMP:       $EXEC_IMPERATIVE"
log "EXEC_INF:       $EXEC_INFORMATIVE"
log "SYNC_CLIENTS:   $SYNC_CLIENTS"
log "STATE_DIR:      $STATE_DIR"

# Variables de ciclo inicializadas para evitar referencias antes de asignación.
LOC_HOST=""
LOC_IP=""
LOC_MAC=""
EXTRA_IMP=""
EXTRA_INF=""

# ---------------------------------------------------------------------------
# run_extra_script: ejecuta un script extra y devuelve JSON normalizado.
# Devuelve "" y exit 0 si no está configurado, falla, o produce JSON inválido.
# En esos casos el campo se envía como null a VAS (COALESCE: conserva valor).
# ---------------------------------------------------------------------------
run_extra_script() {
    local script="$1"
    [[ -z "$script" ]] && return 0

    if [[ ! -x "$script" ]]; then
        log "[EXTRA] Script no ejecutable: $script — omitiendo campo."
        return 0
    fi

    local output
    output="$(timeout 10 "$script" 2>/dev/null)" || {
        log "[EXTRA] Script falló o superó timeout (10s): $script — omitiendo campo."
        return 0
    }

    local normalized
    normalized="$(echo "$output" | jq -c 'to_entries | sort_by(.key) | from_entries' 2>/dev/null)" || {
        log "[EXTRA] Salida de $script no es JSON válido — omitiendo campo."
        return 0
    }

    echo "$normalized"
}

# ---------------------------------------------------------------------------
# collect_extras: rellena EXTRA_IMP y EXTRA_INF según política de configuración.
#
#   EXTRAS_ENABLED=false → "{}" en ambos (borrado explícito en VAS; limpia
#                          cualquier valor previo almacenado en la BD).
#   EXEC_*=cycle         → ejecutar script; "" en fallo (null → COALESCE:
#                          VAS conserva el valor existente ante fallos transitorios).
#   EXEC_*=delegate      → "" (null → COALESCE; vac-register es quien aporta
#                          el JSON cuando el proceso productor lo invoca).
# ---------------------------------------------------------------------------
collect_extras() {
    if [[ "$EXTRAS_ENABLED" != "true" ]]; then
        EXTRA_IMP="{}"
        EXTRA_INF="{}"
        log_debug "[EXTRA] Desactivado → {} en ambos campos (borrado explícito en VAS)."
        return
    fi

    if [[ "$EXEC_IMPERATIVE" == "cycle" ]]; then
        EXTRA_IMP="$(run_extra_script "$EXTRA_IMPERATIVE_SCRIPT")"
    else
        # delegate: VAC envía null para que VAS aplique COALESCE.
        # El proceso externo que tenga los datos llamará a vac-register.
        EXTRA_IMP=""
        log_debug "[EXTRA] extra_imperative: delegate → null (COALESCE en VAS)."
    fi

    if [[ "$EXEC_INFORMATIVE" == "cycle" ]]; then
        EXTRA_INF="$(run_extra_script "$EXTRA_INFORMATIVE_SCRIPT")"
    else
        EXTRA_INF=""
        log_debug "[EXTRA] extra_informative: delegate → null (COALESCE en VAS)."
    fi
}

# ---------------------------------------------------------------------------
# get_remote_version: GET /version → .version, o "" si falla.
# ---------------------------------------------------------------------------
get_remote_version() {
    curl -fsS --max-time 10 --connect-timeout 5 \
        "${VAS_HOST%/}/version" 2>/dev/null \
        | jq -r '.version' 2>/dev/null \
        || echo ""
}

# ---------------------------------------------------------------------------
# selfcheck_local: compara los datos actuales del equipo (LOC_HOST/IP/MAC y
# EXTRA_IMP si EXEC_IMPERATIVE=cycle) contra los valores de identity.json.
#
# Precondición: LOC_HOST, LOC_IP, LOC_MAC y EXTRA_IMP deben estar asignados
# antes de llamar a esta función (lo hace collect_extras + get_hostname/ip/mac).
#
# Devuelve 0 si todo coincide, 1 si hay discordancia o no hay identity.json.
# ---------------------------------------------------------------------------
selfcheck_local() {
    if ! load_identity; then
        log "[SELFCHECK] Sin identity.json — forzando registro."
        return 1
    fi

    local mismatch=0
    [[ "$LOC_HOST" != "$IDENTITY_HOST" ]] && { log "[SELFCHECK] hostname: '$IDENTITY_HOST' → '$LOC_HOST'"; mismatch=1; }
    [[ "$LOC_IP"   != "$IDENTITY_IP"   ]] && { log "[SELFCHECK] IP: '$IDENTITY_IP' → '$LOC_IP'";           mismatch=1; }
    [[ "$LOC_MAC"  != "$IDENTITY_MAC"  ]] && { log "[SELFCHECK] MAC: '$IDENTITY_MAC' → '$LOC_MAC'";        mismatch=1; }

    if [[ "$EXTRAS_ENABLED" == "true" && "$EXEC_IMPERATIVE" == "cycle" ]]; then
        # Solo extra_imperative dispara registro: es el único campo que sube versión en VAS.
        # extra_informative es puramente informativo y nunca provoca bump_version,
        # por lo que su cambio no justifica interrumpir el ciclo de heartbeat.
        # Comparar solo si el script produjo salida (EXTRA_IMP!=""):
        # script fallido → "" → COALESCE en VAS, sin forzar registro innecesario.
        if [[ -n "$EXTRA_IMP" && "$EXTRA_IMP" != "$IDENTITY_IMP" ]]; then
            log "[SELFCHECK] extra_imperative cambió."
            mismatch=1
        fi
    fi

    [[ "$mismatch" -eq 0 ]] && log_debug "[SELFCHECK] Datos locales coinciden con identity.json."
    return "$mismatch"
}

# ---------------------------------------------------------------------------
# refresh_identity: GET /clients/{UUID} → actualiza identity.json con los
# datos confirmados por VAS (incluidos extras almacenados en BD).
# Devuelve 1 si no se puede contactar.
# ---------------------------------------------------------------------------
refresh_identity() {
    local response
    response="$(curl -fsS --max-time 10 --connect-timeout 5 \
        "${VAS_HOST%/}/clients/${CLIENT_ID}" 2>/dev/null)" || response=""

    if [[ -z "$response" ]]; then
        log "[IDENTITY] No se pudo obtener datos propios de VAS. Identity no actualizado."
        return 1
    fi

    local vas_host vas_ip vas_mac vas_imp vas_inf
    vas_host="$(echo "$response" | jq -r '.hostname          // empty' 2>/dev/null || echo "")"
    vas_ip="$(  echo "$response" | jq -r '.ip                // empty' 2>/dev/null || echo "")"
    vas_mac="$( echo "$response" | jq -r '.mac               // empty' 2>/dev/null || echo "")"
    vas_imp="$( echo "$response" | jq -c '.extra_imperative  // empty' 2>/dev/null || echo "")"
    vas_inf="$( echo "$response" | jq -c '.extra_informative // empty' 2>/dev/null || echo "")"

    save_identity "$vas_host" "$vas_ip" "$vas_mac" "$vas_imp" "$vas_inf"
    log_debug "[IDENTITY] Actualizado desde VAS: host=$vas_host ip=$vas_ip"
}

# ---------------------------------------------------------------------------
# Fase de arranque: esperar VAS_HOST y registro inicial (bloqueante)
# ---------------------------------------------------------------------------
# Garantiza que identity.json existe antes de entrar en el bucle principal,
# de modo que el selfcheck siempre puede comparar contra datos conocidos.
# ---------------------------------------------------------------------------
while [[ -z "$VAS_HOST" ]]; do
    log "VAS_HOST no definido en $CONF_FILE. Esperando ${RETRY_SECONDS}s."
    sleep "$RETRY_SECONDS"
done

log "[STARTUP] Registrando equipo en VAS (fase inicial)..."
while true; do
    LOC_HOST="$(get_hostname)"
    LOC_IP="$(get_ip)"
    LOC_MAC="$(get_mac)"
    collect_extras

    if register_client "$LOC_HOST" "$LOC_IP" "$LOC_MAC" "$EXTRA_IMP" "$EXTRA_INF" > /dev/null; then
        save_identity "$LOC_HOST" "$LOC_IP" "$LOC_MAC" "$EXTRA_IMP" "$EXTRA_INF"
        log "[STARTUP] Registro inicial completado."

        remote_ver="$(get_remote_version)"
        if [[ -n "$remote_ver" && "$remote_ver" =~ ^[0-9]+$ ]]; then
            echo "$remote_ver" > "$VERSION_FILE"
            log "[STARTUP] Versión local inicializada: $remote_ver"
        fi

        if [[ "$SYNC_CLIENTS" == "true" ]]; then
            download_clients || log "[STARTUP] No se pudo descargar inventario inicial."
        fi

        # Refrescar identity.json con los valores confirmados por VAS
        # (los extras pueden diferir si VAS aplicó COALESCE sobre valores previos)
        refresh_identity || log "[STARTUP] No se pudo refrescar identity desde VAS. Usando datos locales."
        break
    else
        log "[STARTUP] No se pudo contactar con VAS. Reintentando en ${RETRY_SECONDS}s."
        sleep "$RETRY_SECONDS"
    fi
done

# ---------------------------------------------------------------------------
# Bucle principal
# ---------------------------------------------------------------------------
while true; do
    cycle_start="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
    log_debug "--- Ciclo iniciado: $cycle_start ---"

    # --- Paso 1: Recopilar datos actuales ------------------------------------
    LOC_HOST="$(get_hostname)"
    LOC_IP="$(get_ip)"
    LOC_MAC="$(get_mac)"
    collect_extras   # → EXTRA_IMP, EXTRA_INF

    # --- Paso 2: Selfcheck local + heartbeat o registro ----------------------
    needs_register=false

    if selfcheck_local; then
        # Sin cambios → heartbeat ligero (solo last_seen)
        if ! heartbeat_client; then
            log "[HEARTBEAT] Sin respuesta o cliente no registrado — re-registrando."
            needs_register=true
        fi
    else
        needs_register=true
    fi

    if [[ "$needs_register" == "true" ]]; then
        if ! register_client "$LOC_HOST" "$LOC_IP" "$LOC_MAC" "$EXTRA_IMP" "$EXTRA_INF" > /dev/null; then
            log "No se pudo contactar con VAS. Reintentando en ${RETRY_SECONDS}s."
            sleep "$RETRY_SECONDS"
            continue
        fi
        save_identity "$LOC_HOST" "$LOC_IP" "$LOC_MAC" "$EXTRA_IMP" "$EXTRA_INF"
    fi

    # --- Paso 3: Comparar versiones ------------------------------------------
    local_version="$(tr -d '[:space:]' < "$VERSION_FILE")"
    log_debug "[VERSION] Versión local: $local_version"

    remote_version_raw="$(get_remote_version)"

    if [[ -z "$remote_version_raw" ]]; then
        log "[VERSION] No se pudo obtener versión remota. Reintentando en ${RETRY_SECONDS}s."
        sleep "$RETRY_SECONDS"
        continue
    fi

    if [[ ! "$remote_version_raw" =~ ^[0-9]+$ ]]; then
        log "[VERSION] Versión remota inválida: '$remote_version_raw'. Reintentando en ${RETRY_SECONDS}s."
        sleep "$RETRY_SECONDS"
        continue
    fi

    remote_version="$remote_version_raw"
    log_debug "[VERSION] Versión remota: $remote_version"

    if [[ "$remote_version" == "$local_version" ]]; then
        log_debug "[VERSION] Sin cambios. Próxima comprobación en ${CHECK_SECONDS}s."
        sleep "$CHECK_SECONDS"
        continue
    fi

    log "[SYNC] Nueva versión detectada: $local_version → $remote_version"

    # --- Paso 4: Sincronizar inventario e identity ---------------------------
    if [[ "$SYNC_CLIENTS" == "true" ]]; then
        download_clients || log "[SYNC-ERROR] No se pudo descargar el inventario. Continuando."
    fi

    refresh_identity || log "[IDENTITY] No se pudo refrescar identity desde VAS. Continuando."

    echo "$remote_version" > "$VERSION_FILE"
    log "[SYNC] Versión local actualizada: $remote_version"

    log_debug "--- Ciclo completado. Próxima comprobación en ${CHECK_SECONDS}s. ---"
    sleep "$CHECK_SECONDS"
done
