#!/bin/bash
# /usr/local/bin/nginx-user-config-watcher.sh
# Watcher para configs nginx de usuarios via SFTP
# Detecta cualquier archivo *.conf en /home/USER/html/
# Con notificaciones XMPP a jla@librebits.info
#
# v2 — Análisis CSP post-deploy + humanización de errores nginx (P2.5)
# v3 — Multi-sitio por alumno: mi-nginx-SUFIJO.conf → USER-SUFIJO.qu3v3d0.tech
#       Retrocompatible: mi-nginx.conf (sin sufijo) funciona igual que v2

WATCH_DIR="/home"
LOG="/var/log/nginx-user-configs.log"
MAX_SITES=4

log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG"
}

# Notificación XMPP (fire and forget)
notify_xmpp() {
    local msg="$1"
    /usr/local/bin/xmpp-notify.py "$msg" >/dev/null 2>&1 &
}

# -------------------------------------------------------
# analyze_csp()  —  Analiza un .conf desplegado buscando
#   cabeceras CSP y de seguridad. Devuelve texto con pistas.
# -------------------------------------------------------
analyze_csp() {
    local conf="$1"
    local hints=""
    local conf_content
    conf_content=$(cat "$conf" 2>/dev/null) || return

    # --- Check 1: ¿Tiene cabecera CSP? ---
    if ! echo "$conf_content" | grep -q "Content-Security-Policy"; then
        hints="${hints}💡 Tu .conf no tiene cabecera Content-Security-Policy (opcional según la práctica)
"
    else
        # --- Check 2: ¿CSP multiline? (add_header ... " sin cierre en misma línea) ---
        # Buscamos líneas con add_header Content-Security-Policy que abren comillas
        # pero no las cierran en la misma línea
        if echo "$conf_content" | grep -E 'add_header\s+Content-Security-Policy' | grep -qE '"[^"]*$'; then
            hints="${hints}⚠️ CSP con saltos de línea: ponlo todo en UNA sola línea entre comillas
"
        fi

        # --- Check 3: ¿CSP en bloque HTTPS (443) o solo en HTTP (80)? ---
        # Estrategia: buscar si CSP aparece ANTES del primer listen 443
        # o si no hay listen 443 en absoluto
        local has_443
        has_443=$(echo "$conf_content" | grep -c "listen.*443")
        if [[ "$has_443" -eq 0 ]]; then
            hints="${hints}⚠️ No hay bloque HTTPS (listen 443) — la CSP debería ir en el bloque HTTPS
"
        else
            # Comprobar si CSP está solo en bloque HTTP (antes de listen 443)
            # Obtenemos la línea de CSP y la línea de listen 443
            local csp_line
            local ssl_line
            csp_line=$(echo "$conf_content" | grep -n "Content-Security-Policy" | head -1 | cut -d: -f1)
            ssl_line=$(echo "$conf_content" | grep -n "listen.*443" | head -1 | cut -d: -f1)
            if [[ -n "$csp_line" && -n "$ssl_line" && "$csp_line" -lt "$ssl_line" ]]; then
                # CSP aparece antes del bloque 443 — verificar si también está después
                local csp_after_ssl
                csp_after_ssl=$(echo "$conf_content" | tail -n +"$ssl_line" | grep -c "Content-Security-Policy")
                if [[ "$csp_after_ssl" -eq 0 ]]; then
                    hints="${hints}⚠️ CSP solo en bloque HTTP (80), no en HTTPS (443) — ponla en el bloque donde se sirve tu web
"
                fi
            fi
        fi

        # --- Check 4: ¿unsafe-inline en script-src? ---
        if echo "$conf_content" | grep -i "script-src" | grep -qi "unsafe-inline"; then
            hints="${hints}⚠️ unsafe-inline en script-src debilita la protección CSP contra XSS
"
        fi

        # --- Check 5: ¿Falta 'always' en add_header CSP? ---
        if echo "$conf_content" | grep "Content-Security-Policy" | grep -qv "always"; then
            hints="${hints}💡 Consejo: añade 'always' para que CSP se envíe también en respuestas 4xx/5xx
"
        fi
    fi

    # --- Check 6: ¿Tiene las otras cabeceras de seguridad? ---
    local missing_headers=""
    if ! echo "$conf_content" | grep -q "X-Content-Type-Options"; then
        missing_headers="${missing_headers} X-Content-Type-Options"
    fi
    if ! echo "$conf_content" | grep -q "X-Frame-Options"; then
        missing_headers="${missing_headers} X-Frame-Options"
    fi
    if ! echo "$conf_content" | grep -q "X-XSS-Protection"; then
        missing_headers="${missing_headers} X-XSS-Protection"
    fi
    if ! echo "$conf_content" | grep -q "Referrer-Policy"; then
        missing_headers="${missing_headers} Referrer-Policy"
    fi
    if [[ -n "$missing_headers" ]]; then
        hints="${hints}💡 Faltan cabeceras de seguridad:${missing_headers}
"
    fi

    # Devolver resultado
    if [[ -z "$hints" ]]; then
        echo "🔒 CSP y cabeceras de seguridad: todo OK"
    else
        echo "$hints"
    fi
}

# -------------------------------------------------------
# humanize_nginx_error()  —  Traduce errores nginx -t
#   a pistas comprensibles para el alumno.
# -------------------------------------------------------
humanize_nginx_error() {
    local error_output="$1"
    local pistas=""

    if echo "$error_output" | grep -q 'unexpected "}"'; then
        pistas="${pistas}💡 ¿Te falta un ';' al final de alguna directiva?
"
    fi
    if echo "$error_output" | grep -q "unexpected end of file"; then
        pistas="${pistas}💡 ¿Cerraste todos los bloques server { } con '}'?
"
    fi
    if echo "$error_output" | grep -q "directive is not allowed"; then
        pistas="${pistas}💡 Revisa que cada directiva esté dentro del bloque server { } correcto
"
    fi
    if echo "$error_output" | grep -q "unknown directive"; then
        pistas="${pistas}💡 ¿Hay algún typo en el nombre de una directiva?
"
    fi
    if echo "$error_output" | grep -q "invalid number of arguments"; then
        pistas="${pistas}💡 Alguna directiva tiene argumentos de más o de menos — revisa comillas y punto y coma
"
    fi
    if echo "$error_output" | grep -qE '(missing|unexpected) ";"'; then
        pistas="${pistas}💡 Revisa los punto y coma (;) — puede que sobre o falte alguno
"
    fi

    echo "$pistas"
}

# -------------------------------------------------------
# check_https_redirect()  —  Verifica si la config tiene
#   redirección HTTP → HTTPS (return 301 https://)
# -------------------------------------------------------
check_https_redirect() {
    local conf="$1"
    local expected_name="$2"
    local conf_content
    conf_content=$(cat "$conf" 2>/dev/null) || return
    local hints=""

    # ¿Tiene listen 443 ssl? (es decir, ¿sirve HTTPS?)
    if echo "$conf_content" | grep -q "listen.*443.*ssl"; then
        # Tiene HTTPS → ¿redirige HTTP a HTTPS?
        if ! echo "$conf_content" | grep -qE "return\s+301\s+https://"; then
            hints="${hints}⚠️ HTTPS activo pero NO hay redirección HTTP → HTTPS.
💡 Añade un bloque server { listen 80; return 301 https://\$server_name\$request_uri; }
"
        else
            hints="${hints}✅ Redirección HTTP → HTTPS configurada correctamente
"
        fi
    else
        # No tiene HTTPS
        hints="${hints}⚠️ Tu config no tiene bloque HTTPS (listen 443 ssl) — el tráfico no va cifrado
💡 Recuerda: la práctica requiere forzar tráfico HTTP → HTTPS
"
    fi

    echo "$hints"
}

# -------------------------------------------------------
# check_naming_convention()  —  Avisa si el fichero .conf
#   no sigue la nomenclatura mi-nginx[-SUFIJO].conf
# -------------------------------------------------------
check_naming_convention() {
    local conffile="$1"
    if [[ ! "$conffile" =~ ^mi-nginx(-[a-z0-9]+)?\.conf$ ]]; then
        echo "📛 Tu fichero se llama '$conffile' — la nomenclatura recomendada es:
   • mi-nginx.conf              → sitio principal
   • mi-nginx-hextris.conf      → segundo sitio (Hextris)
   • mi-nginx-app.conf          → tercer sitio (App)
   Usa el formato mi-nginx-NOMBRE.conf para que el sistema detecte automáticamente tu subdominio.
"
    fi
}

# -------------------------------------------------------
# extract_app_suffix()  —  Extrae el sufijo de app del nombre del .conf
#   mi-nginx.conf         → "" (vacío = sitio principal)
#   mi-nginx-portfolio.conf → "portfolio"
#   otro-nombre.conf      → "" (no sigue el patrón multi-sitio)
# -------------------------------------------------------
extract_app_suffix() {
    local conffile="$1"
    if [[ "$conffile" =~ ^mi-nginx-([a-z0-9]+)\.conf$ ]]; then
        echo "${BASH_REMATCH[1]}"
    else
        echo ""
    fi
}

# -------------------------------------------------------
# undeploy_site()  —  Elimina un site de nginx cuando el
#   alumno borra su mi-nginx[-SUFIJO].conf via SFTP
# -------------------------------------------------------
undeploy_site() {
    local user="$1"
    local conffile="$2"

    local app_suffix
    app_suffix=$(extract_app_suffix "$conffile")

    local site_id
    if [[ -n "$app_suffix" ]]; then
        site_id="${user}-${app_suffix}"
    else
        site_id="${user}"
    fi

    local dest="/etc/nginx/sites-available/$site_id"
    local enabled="/etc/nginx/sites-enabled/$site_id"

    # Solo actuar si el site existe desplegado
    if [[ -f "$dest" || -L "$enabled" ]]; then
        rm -f "$enabled" "$dest"

        # Validar que nginx sigue OK sin ese site
        if nginx -t 2>&1 | grep -q "successful"; then
            systemctl reload nginx
            log "[$user] UNDEPLOY: Site $site_id eliminado (borrado $conffile) y nginx recargado"
            notify_xmpp "🗑️ [$user] Site $site_id eliminado (borrado $conffile)"
        else
            log "[$user] UNDEPLOY: Site $site_id eliminado pero nginx -t falló (recargando de todas formas)"
            systemctl reload nginx
        fi
    fi
}

validate_and_deploy() {
    local user="$1"
    local conffile="$2"
    local src="/home/$user/html/$conffile"

    # --- v3: Determinar sufijo de app y nombres esperados ---
    local app_suffix
    app_suffix=$(extract_app_suffix "$conffile")

    local expected_name
    local expected_root
    local site_id

    if [[ -n "$app_suffix" ]]; then
        expected_name="${user}-${app_suffix}"
        expected_root="/home/$user/html/$app_suffix"
        site_id="${user}-${app_suffix}"
    else
        expected_name="${user}"
        expected_root="/home/$user/html"
        site_id="${user}"
    fi

    local dest="/etc/nginx/sites-available/$site_id"
    local enabled="/etc/nginx/sites-enabled/$site_id"
    local tmp="/tmp/nginx-test-${site_id}.conf"
    local backup="/tmp/nginx-backup-${site_id}.conf"
    local error_file="/home/$user/html/nginx-error.log"
    local status_file="/home/$user/html/nginx-status.log"
    local practica_file="/home/$user/html/practica-status.log"

    # Verificar que el archivo existe
    [[ ! -f "$src" ]] && return 1

    # --- v3: Límite de sitios por alumno (solo para sitios NUEVOS) ---
    if [[ -n "$app_suffix" && ! -f "$dest" ]]; then
        local current_sites
        current_sites=$(ls /etc/nginx/sites-enabled/${user} /etc/nginx/sites-enabled/${user}-* 2>/dev/null | wc -l)
        if (( current_sites >= MAX_SITES )); then
            log "[$user] RECHAZADO: límite de $MAX_SITES sitios alcanzado"
            echo "ERROR: Ya tienes $MAX_SITES sitios desplegados (límite máximo $MAX_SITES)." > "$error_file"
            echo "Elimina algún mi-nginx-*.conf si quieres crear uno nuevo." >> "$error_file"
            chown $user:www-data "$error_file" 2>/dev/null
            notify_xmpp "❌ [$user] Config RECHAZADA: límite de $MAX_SITES sitios alcanzado"
            return 1
        fi
    fi

    # Copiar a temporal para validar
    cp "$src" "$tmp"

    # Comprobar nomenclatura del fichero (aviso temprano)
    local naming_check
    naming_check=$(check_naming_convention "$conffile")

    # SEGURIDAD: Verificar que server_name coincide con el esperado
    if ! grep -qE "^\s*server_name\s+${expected_name}\.(local|qu3v3d0\.tech)" "$tmp"; then
        log "[$user] RECHAZADO: server_name no coincide (esperado: ${expected_name}.qu3v3d0.tech)"
        {
            echo "ERROR: server_name debe ser ${expected_name}.qu3v3d0.tech"
            if [[ -n "$naming_check" ]]; then
                echo ""
                echo "$naming_check"
            fi
        } > "$error_file"
        if [[ -n "$app_suffix" ]]; then
            echo "(Para el fichero $conffile el subdominio esperado es ${expected_name}.qu3v3d0.tech)" >> "$error_file"
        fi
        chown $user:www-data "$error_file" 2>/dev/null
        notify_xmpp "❌ [$user] Config RECHAZADA: server_name incorrecto ($conffile, esperado: ${expected_name})"
        rm -f "$tmp"
        return 1
    fi

    # SEGURIDAD: Verificar que root apunta al directorio correcto
    # - Sin sufijo: prefijo /home/USER/html (v2-compatible, permite subdirectorios)
    # - Con sufijo: exacto /home/USER/html/SUFIJO (multi-sitio v3)
    if [[ -n "$app_suffix" ]]; then
        if ! grep -qE "^\s*root\s+${expected_root}\s*;" "$tmp"; then
            log "[$user] RECHAZADO: root no apunta a $expected_root"
            echo "ERROR: root debe ser ${expected_root}" > "$error_file"
            echo "(Para el fichero $conffile la carpeta debe ser html/$app_suffix/)" >> "$error_file"
            chown $user:www-data "$error_file" 2>/dev/null
            notify_xmpp "❌ [$user] Config RECHAZADA: root incorrecto ($conffile, esperado: ${expected_root})"
            rm -f "$tmp"
            return 1
        fi
    else
        # Retrocompatible con v2: acepta /home/USER/html y cualquier subdirectorio
        if ! grep -qE "^\s*root\s+/home/$user/html" "$tmp"; then
            log "[$user] RECHAZADO: root no apunta a /home/$user/html"
            echo "ERROR: root debe ser /home/$user/html" > "$error_file"
            chown $user:www-data "$error_file" 2>/dev/null
            notify_xmpp "❌ [$user] Config RECHAZADA: root incorrecto ($conffile)"
            rm -f "$tmp"
            return 1
        fi
    fi

    # --- v3: Verificar que el subdirectorio existe (solo multi-sitio) ---
    if [[ -n "$app_suffix" && ! -d "$expected_root" ]]; then
        log "[$user] RECHAZADO: directorio $expected_root no existe"
        echo "ERROR: La carpeta '$app_suffix/' no existe dentro de html/." > "$error_file"
        echo "Crea primero la carpeta via SFTP (FileZilla) y luego vuelve a subir $conffile." >> "$error_file"
        chown $user:www-data "$error_file" 2>/dev/null
        notify_xmpp "❌ [$user] Config RECHAZADA: carpeta html/$app_suffix/ no existe"
        rm -f "$tmp"
        return 1
    fi

    # SEGURIDAD: Bloquear directivas peligrosas (ignorando comentarios)
    if grep -v "^\s*#" "$tmp" | grep -qE "(proxy_pass|fastcgi_pass|uwsgi_pass|include\s+/|lua_|perl_|upstream)" ; then
        log "[$user] RECHAZADO: directivas prohibidas detectadas ($conffile)"
        echo "ERROR: Directivas proxy/include/lua no permitidas" > "$error_file"
        chown $user:www-data "$error_file" 2>/dev/null
        notify_xmpp "❌ [$user] Config RECHAZADA: directivas prohibidas ($conffile)"
        rm -f "$tmp"
        return 1
    fi

    # Hacer backup del config actual si existe
    [[ -f "$dest" ]] && cp "$dest" "$backup"

    # Copiar nuevo config a sites-available
    cp "$tmp" "$dest"

    # Asegurar que el symlink existe
    ln -sf "$dest" "$enabled"

    # Validar sintaxis nginx con la nueva config
    local nginx_output
    nginx_output=$(nginx -t 2>&1)

    if echo "$nginx_output" | grep -q "successful"; then
        # Sintaxis OK - recargar nginx
        systemctl reload nginx
        log "[$user] OK: Config desplegada ($conffile → $site_id) y nginx recargado"

        # Analizar CSP y cabeceras de seguridad
        local csp_analysis
        csp_analysis=$(analyze_csp "$dest")

        # Analizar redirección HTTP → HTTPS
        local https_check
        https_check=$(check_https_redirect "$dest" "$expected_name")

        # Mensaje de deploy según sea sitio principal o sub-sitio
        local deploy_msg
        if [[ -n "$app_suffix" ]]; then
            deploy_msg="✅ [$user] Config desplegada OK ($conffile → ${expected_name}.qu3v3d0.tech)"
        else
            deploy_msg="✅ [$user] Config desplegada OK ($conffile)"
        fi

        # XMPP al profesor (resumen + todos los análisis)
        notify_xmpp "$deploy_msg
$https_check$csp_analysis${naming_check:+
$naming_check}"

        # nginx-status.log (resumen para el alumno)
        {
            echo "OK: Config desplegada desde $conffile $(date)"
            if [[ -n "$app_suffix" ]]; then
                echo "🌐 Sitio: https://${expected_name}.qu3v3d0.tech"
            fi
            echo ""
            echo "$https_check"
            echo "$csp_analysis"
            if [[ -n "$naming_check" ]]; then
                echo "$naming_check"
            fi
        } > "$status_file"
        chown $user:www-data "$status_file" 2>/dev/null

        # practica-status.log (detalle completo)
        {
            echo "=== Análisis de $conffile — $(date) ==="
            if [[ -n "$app_suffix" ]]; then
                echo "🌐 Sitio: https://${expected_name}.qu3v3d0.tech"
                echo "📁 Root: $expected_root"
            fi
            echo ""
            echo "--- Redirección HTTPS ---"
            echo "$https_check"
            echo "--- Seguridad (CSP y cabeceras) ---"
            echo "$csp_analysis"
            if [[ -n "$naming_check" ]]; then
                echo "--- Nomenclatura ---"
                echo "$naming_check"
            fi
            echo "---"
            echo "Revisa el enunciado de tu práctica para los entregables pendientes."
        } > "$practica_file"
        chown $user:www-data "$practica_file" 2>/dev/null

        rm -f "$error_file"
        rm -f "$backup"
    else
        # Error de sintaxis - revertir
        log "[$user] RECHAZADO: Error de sintaxis nginx ($conffile → $site_id)"

        # Humanizar el error
        local pistas
        pistas=$(humanize_nginx_error "$nginx_output")

        # nginx-error.log con error original + pistas
        {
            echo "$nginx_output" | tail -5
            echo "ERROR: Sintaxis nginx inválida."
            if [[ -n "$pistas" ]]; then
                echo ""
                echo "--- Pistas ---"
                echo "$pistas"
            fi
        } > "$error_file"
        chown $user:www-data "$error_file" 2>/dev/null

        # XMPP al profesor con pistas
        if [[ -n "$pistas" ]]; then
            notify_xmpp "❌ [$user] Config RECHAZADA: sintaxis inválida ($conffile)
$pistas"
        else
            notify_xmpp "❌ [$user] Config RECHAZADA: sintaxis inválida ($conffile)"
        fi

        # Restaurar backup si existe
        if [[ -f "$backup" ]]; then
            cp "$backup" "$dest"
            log "[$user] Config anterior de $site_id restaurada"
        else
            rm -f "$dest" "$enabled"
            log "[$user] Config $site_id eliminada (no había backup)"
        fi
        rm -f "$backup"
    fi

    rm -f "$tmp"
}

log "=== Nginx User Config Watcher v3 iniciado (*.conf, multi-sitio) ==="
notify_xmpp "🚀 [zzz] Nginx Config Watcher v3 iniciado (*.conf, multi-sitio)"

# Bucle principal con inotifywait - detecta cualquier *.conf
# v3.1: también detecta delete/moved_from para limpiar sites huérfanos
inotifywait -m -r -e close_write,moved_to,delete,moved_from --format "%e %w%f" "$WATCH_DIR" 2>/dev/null | while read event filepath; do
    # Captura: /home/USER/html/ARCHIVO.conf
    if [[ "$filepath" =~ ^/home/([^/]+)/html/([^/]+.conf)$ ]]; then
        user="${BASH_REMATCH[1]}"
        conffile="${BASH_REMATCH[2]}"
        if [[ "$event" == *DELETE* || "$event" == *MOVED_FROM* ]]; then
            log "[$user] Detectado borrado de $conffile"
            undeploy_site "$user" "$conffile"
        else
            log "[$user] Detectado cambio en $conffile"
            sleep 1  # Esperar a que el archivo esté completo
            validate_and_deploy "$user" "$conffile"
        fi
    fi
done
