#!/bin/bash
#
# NimiApps · Actualizador de ENT-002 (v0.2.0) a ENT-003 (v0.3.0)
#
# Uso:
#   bash actualizar.sh RUTA_DE_LA_INSTALACION --simular   (solo comprueba, no cambia nada)
#   bash actualizar.sh RUTA_DE_LA_INSTALACION             (actualiza los archivos)
#
# Qué hace:
#   1. Comprueba la integridad del paquete (manifiesto/paquete.sha256).
#   2. Comprueba que la ruta es una instalación de NimiApps 0.2.0 con .env y vendor/.
#   3. Exige que la aplicación esté en modo mantenimiento (php artisan down).
#   4. Comprueba que los archivos que se van a sustituir no tienen cambios locales.
#   5. Guarda una copia de los archivos que sustituye FUERA de public_html.
#   6. Copia los archivos nuevos y modificados (config/microapps.php el último).
#      Incluye la nueva dependencia stripe/stripe-php en vendor/stripe/ y los
#      5 archivos del cargador de Composer (vendor/composer/) que la registran.
#      El resto de vendor/ no cambia.
#   7. Verifica las huellas SHA-256 de lo copiado.
#
# Qué NO hace (a propósito):
#   - No toca .env, la base de datos, storage/, bootstrap/cache/,
#     .htaccess ni public/.htaccess, ni el resto de vendor/.
#   - No ejecuta migraciones ni borra nada: esos pasos se hacen a mano
#     siguiendo ACTUALIZACION-CPANEL-ENT-003.md.
#   - No hace copia de la base de datos (se hace antes con mysqldump).
#
# Se detiene ante el primer error. Compatible con bash 4.2 (CentOS 7).

set -Eeuo pipefail
umask 022

VERSION_ORIGEN="0.2.0"
VERSION_DESTINO="0.3.0"

PAQUETE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CARGA="$PAQUETE/archivos"
MANIFIESTO="$PAQUETE/manifiesto"

# ---------------------------------------------------------------- utilidades

rojo()  { printf '\033[31m%s\033[0m\n' "$*"; }
verde() { printf '\033[32m%s\033[0m\n' "$*"; }
info()  { printf '  %s\n' "$*"; }
paso()  { printf '\n\033[1m== %s\033[0m\n' "$*"; }
error() { rojo "ERROR: $*"; rojo "La actualización se ha detenido."; exit 1; }

uso() {
    echo "Uso: bash actualizar.sh RUTA_DE_LA_INSTALACION [--simular]"
    echo "Ejemplo: bash actualizar.sh /home/usuario/public_html/staging.nimiapps.com --simular"
    exit 2
}

# Versión declarada en config/microapps.php ('version' => 'x.y.z').
leer_version() {
    sed -n "s/^[[:space:]]*'version'[[:space:]]*=>[[:space:]]*'\([0-9][0-9.]*\)'.*/\1/p" "$1" | head -n 1
}

huella() { sha256sum "$1" | awk '{print $1}'; }

# Huella esperada de un archivo en un listado "hash  ruta".
huella_en() {
    awk -v f="$2" '{ h=$1; $1=""; sub(/^ +/, ""); if ($0 == f) { print h; exit } }' "$1"
}

# ---------------------------------------------------------------- argumentos

DESTINO=""
SIMULAR=0
for arg in "$@"; do
    case "$arg" in
        --simular) SIMULAR=1 ;;
        -h|--help|--ayuda) uso ;;
        -*) echo "Opción desconocida: $arg"; uso ;;
        *) [ -z "$DESTINO" ] && DESTINO="$arg" || uso ;;
    esac
done
[ -n "$DESTINO" ] || uso

echo "NimiApps · actualización $VERSION_ORIGEN -> $VERSION_DESTINO (ENT-003)"
[ "$SIMULAR" -eq 1 ] && echo "MODO SIMULACIÓN: no se modificará ningún archivo."

# ---------------------------------------------------------------- 1. entorno

paso "1. Entorno"
[ "$(id -u)" -ne 0 ] || error "no ejecutes este script como root; usa el usuario de la cuenta de cPanel."
for herramienta in sha256sum awk sed cp mv mkdir date; do
    command -v "$herramienta" >/dev/null 2>&1 || error "falta la herramienta '$herramienta'."
done

PHP_BIN="${PHP:-php}"
command -v "$PHP_BIN" >/dev/null 2>&1 || error "no se encuentra PHP ('$PHP_BIN'). Indica la ruta: PHP=/opt/cpanel/ea-php83/root/usr/bin/php bash actualizar.sh ..."
PHP_VERSION="$("$PHP_BIN" -r 'echo PHP_VERSION;')"
"$PHP_BIN" -r 'exit(version_compare(PHP_VERSION, "8.3.0", ">=") ? 0 : 1);' \
    || error "PHP $PHP_VERSION es demasiado antiguo (se necesita 8.3 o superior). Usa PHP=/opt/cpanel/ea-php83/root/usr/bin/php"
info "Usuario: $(id -un) · PHP $PHP_VERSION ($PHP_BIN)"

# ---------------------------------------------------------------- 2. paquete

paso "2. Integridad del paquete"
for f in paquete.sha256 archivos-nuevos.txt archivos-modificados.txt v020-esperado.sha256; do
    [ -f "$MANIFIESTO/$f" ] || error "falta manifiesto/$f. Descomprime de nuevo el ZIP."
done
[ -d "$CARGA" ] || error "falta la carpeta archivos/. Descomprime de nuevo el ZIP."
( cd "$CARGA" && sha256sum -c --quiet "$MANIFIESTO/paquete.sha256" ) \
    || error "el contenido del paquete no coincide con manifiesto/paquete.sha256 (ZIP dañado o modificado)."
TOTAL_NUEVOS=$(grep -c . "$MANIFIESTO/archivos-nuevos.txt" || true)
TOTAL_MODIFICADOS=$(grep -c . "$MANIFIESTO/archivos-modificados.txt" || true)
info "Paquete correcto: $TOTAL_NUEVOS archivos nuevos y $TOTAL_MODIFICADOS modificados."

# ---------------------------------------------------------------- 3. destino

paso "3. Instalación de destino"
[ -d "$DESTINO" ] || error "no existe la carpeta '$DESTINO'."
DESTINO="$(cd "$DESTINO" && pwd -P)"
info "Ruta: $DESTINO"
case "$PAQUETE/" in
    "$DESTINO"/*) error "el paquete está descomprimido dentro de la instalación. Descomprímelo fuera (por ejemplo en tu carpeta personal)." ;;
esac
[ -f "$DESTINO/artisan" ]               || error "no hay 'artisan' en $DESTINO: no parece una instalación de NimiApps."
[ -f "$DESTINO/config/microapps.php" ]  || error "no existe config/microapps.php en $DESTINO."
[ -f "$DESTINO/vendor/autoload.php" ]   || error "no existe vendor/autoload.php en $DESTINO."
[ -f "$DESTINO/.env" ]                  || error "no existe .env en $DESTINO (este paquete no instala desde cero)."
[ -d "$DESTINO/public" ]                || error "no existe la carpeta public/ en $DESTINO."
[ -w "$DESTINO" ]                       || error "el usuario $(id -un) no puede escribir en $DESTINO."

VERSION_INSTALADA="$(leer_version "$DESTINO/config/microapps.php")"
[ -n "$VERSION_INSTALADA" ] || error "no se puede leer la versión de config/microapps.php."
info "Versión instalada: $VERSION_INSTALADA"

if [ "$VERSION_INSTALADA" = "$VERSION_DESTINO" ]; then
    # ¿Ya se aplicó este mismo paquete? Se comprueba que todo coincide.
    if ( cd "$DESTINO" && cat "$MANIFIESTO/archivos-nuevos.txt" "$MANIFIESTO/archivos-modificados.txt" \
         | while IFS= read -r f; do printf '%s  %s\n' "$(huella_en "$MANIFIESTO/paquete.sha256" "$f")" "$f"; done \
         | sha256sum -c --quiet - >/dev/null 2>&1 ); then
        verde "Los archivos de la versión $VERSION_DESTINO ya están instalados y coinciden con este paquete."
        echo "No hay nada que copiar. Continúa con el paso de migraciones del manual si aún no lo has hecho."
        exit 0
    fi
    error "la versión instalada ya es $VERSION_DESTINO pero hay archivos que no coinciden con este paquete. Revisa la instalación antes de continuar."
fi
[ "$VERSION_INSTALADA" = "$VERSION_ORIGEN" ] \
    || error "este paquete solo actualiza desde la versión $VERSION_ORIGEN (instalada: $VERSION_INSTALADA)."

# ---------------------------------------------------------------- 4. mantenimiento

paso "4. Modo mantenimiento"
if [ -f "$DESTINO/storage/framework/down" ]; then
    info "La aplicación está en modo mantenimiento."
elif [ "$SIMULAR" -eq 1 ]; then
    info "AVISO: la aplicación NO está en modo mantenimiento. En la actualización real será obligatorio (php artisan down)."
else
    error "la aplicación no está en modo mantenimiento. Ejecuta antes: $PHP_BIN artisan down"
fi

# ---------------------------------------------------------------- 5. cambios locales

paso "5. Comprobación de cambios locales"
CONFLICTOS=0
while IFS= read -r f; do
    [ -n "$f" ] || continue
    destino_f="$DESTINO/$f"
    nueva="$(huella_en "$MANIFIESTO/paquete.sha256" "$f")"
    esperada="$(huella_en "$MANIFIESTO/v020-esperado.sha256" "$f")"
    if [ ! -f "$destino_f" ]; then
        rojo "  Falta en la instalación: $f"; CONFLICTOS=$((CONFLICTOS + 1)); continue
    fi
    actual="$(huella "$destino_f")"
    if [ "$actual" != "$esperada" ] && [ "$actual" != "$nueva" ]; then
        rojo "  Modificado localmente: $f"; CONFLICTOS=$((CONFLICTOS + 1))
    fi
done < "$MANIFIESTO/archivos-modificados.txt"

while IFS= read -r f; do
    [ -n "$f" ] || continue
    destino_f="$DESTINO/$f"
    if [ -e "$destino_f" ]; then
        nueva="$(huella_en "$MANIFIESTO/paquete.sha256" "$f")"
        if [ ! -f "$destino_f" ] || [ "$(huella "$destino_f")" != "$nueva" ]; then
            rojo "  Ya existe y es distinto: $f"; CONFLICTOS=$((CONFLICTOS + 1))
        fi
    fi
done < "$MANIFIESTO/archivos-nuevos.txt"

if [ "$CONFLICTOS" -gt 0 ]; then
    error "$CONFLICTOS archivo(s) de la instalación no coinciden con la versión 0.2.0. No se sobrescribe nada; revisa esos archivos (guárdalos aparte o restáuralos desde la 0.2.0) y vuelve a ejecutar."
fi
info "Sin cambios locales en los archivos afectados."
info "No se tocan: .env, .htaccess, public/.htaccess, storage/, bootstrap/cache/, base de datos."
info "De vendor/ solo se añade vendor/stripe/ y se sustituyen 5 archivos de vendor/composer/."

# ---------------------------------------------------------------- simulación

if [ "$SIMULAR" -eq 1 ]; then
    paso "Simulación terminada"
    echo "  Se copiarían $TOTAL_NUEVOS archivos nuevos y se sustituirían $TOTAL_MODIFICADOS:"
    sed 's/^/    ~ /' "$MANIFIESTO/archivos-modificados.txt"
    echo "  (lista de nuevos en manifiesto/archivos-nuevos.txt)"
    verde "Todo listo para actualizar. No se ha modificado nada."
    exit 0
fi

# ---------------------------------------------------------------- 6. copia de seguridad

paso "6. Copia de seguridad de los archivos que se sustituyen"
RAIZ_RESPALDOS="${NIMIAPPS_RESPALDOS:-$HOME/nimiapps-respaldos}"
mkdir -p "$RAIZ_RESPALDOS"
RAIZ_RESPALDOS="$(cd "$RAIZ_RESPALDOS" && pwd -P)"
case "$RAIZ_RESPALDOS/" in
    "$DESTINO"/*|*/public_html/*) error "la carpeta de copias ($RAIZ_RESPALDOS) no puede estar dentro de public_html ni de la instalación." ;;
esac
RESPALDO="$RAIZ_RESPALDOS/actualizacion-ent003-$(date +%Y%m%d-%H%M%S)"
[ ! -e "$RESPALDO" ] || error "ya existe $RESPALDO; espera un segundo y repite."
mkdir -p "$RESPALDO/archivos"
chmod 700 "$RAIZ_RESPALDOS" "$RESPALDO"
REGISTRO="$RESPALDO/registro.txt"
{
    echo "Actualización NimiApps $VERSION_ORIGEN -> $VERSION_DESTINO"
    echo "Fecha: $(date '+%Y-%m-%d %H:%M:%S %z')"
    echo "Instalación: $DESTINO"
    echo "Usuario: $(id -un)"
} > "$REGISTRO"

while IFS= read -r f; do
    [ -n "$f" ] || continue
    mkdir -p "$RESPALDO/archivos/$(dirname "$f")"
    cp -p "$DESTINO/$f" "$RESPALDO/archivos/$f"
done < "$MANIFIESTO/archivos-modificados.txt"
( cd "$RESPALDO/archivos" && find . -type f | sed 's|^\./||' | LC_ALL=C sort | while IFS= read -r f; do sha256sum "$f"; done ) > "$RESPALDO/respaldo.sha256"

# Archivos nuevos que no existían (los que habría que quitar para volver a 0.2.0).
: > "$RESPALDO/archivos-nuevos-creados.txt"
while IFS= read -r f; do
    [ -n "$f" ] || continue
    [ -e "$DESTINO/$f" ] || echo "$f" >> "$RESPALDO/archivos-nuevos-creados.txt"
done < "$MANIFIESTO/archivos-nuevos.txt"

cat > "$RESPALDO/restaurar-archivos.sh" <<RESTAURAR
#!/bin/bash
# Devuelve los ARCHIVOS de la instalación a como estaban antes de la actualización.
# No toca la base de datos: antes hay que deshacer las migraciones de ENT-003 o
# restaurar la copia de la base de datos (ver ACTUALIZACION-CPANEL-ENT-003.md).
set -Eeuo pipefail
DESTINO="$DESTINO"
RESPALDO="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
[ -f "\$DESTINO/storage/framework/down" ] || { echo "Pon antes la aplicación en mantenimiento (php artisan down)."; exit 1; }
( cd "\$RESPALDO/archivos" && sha256sum -c --quiet "\$RESPALDO/respaldo.sha256" ) || { echo "La copia está dañada; no se restaura nada."; exit 1; }
echo "Se restaurarán los archivos de \$RESPALDO/archivos en \$DESTINO"
echo "y se borrarán los \$(grep -c . "\$RESPALDO/archivos-nuevos-creados.txt" || true) archivos que creó la actualización."
read -r -p "Escribe RESTAURAR para continuar: " respuesta
[ "\$respuesta" = "RESTAURAR" ] || { echo "Cancelado."; exit 1; }
( cd "\$RESPALDO/archivos" && find . -type f | sed 's|^\./||' ) | while IFS= read -r f; do
    mkdir -p "\$DESTINO/\$(dirname "\$f")"
    cp -p "\$RESPALDO/archivos/\$f" "\$DESTINO/\$f.restaurar.\$\$"
    mv -f "\$DESTINO/\$f.restaurar.\$\$" "\$DESTINO/\$f"
done
while IFS= read -r f; do
    [ -n "\$f" ] || continue
    rm -f "\$DESTINO/\$f"
done < "\$RESPALDO/archivos-nuevos-creados.txt"
echo "Archivos restaurados. Sigue el manual: config:clear, route:clear, view:clear, event:clear, optimize y microapps:check."
RESTAURAR
chmod 700 "$RESPALDO/restaurar-archivos.sh"
info "Copia guardada en: $RESPALDO"
echo "Copia de seguridad: $RESPALDO" >> "$REGISTRO"

# ---------------------------------------------------------------- 7. copia de archivos

paso "7. Copia de los archivos de la versión $VERSION_DESTINO"
fallo_copia() {
    rojo "ERROR durante la copia de archivos. La instalación puede haber quedado a medias."
    rojo "La aplicación sigue en modo mantenimiento. Opciones:"
    rojo "  - Corregir el problema (espacio, permisos) y volver a ejecutar este script."
    rojo "  - Volver a los archivos anteriores con: bash $RESPALDO/restaurar-archivos.sh"
    echo "ERROR durante la copia" >> "$REGISTRO"
}
trap fallo_copia ERR

copiar() {
    local f="$1"
    local dir
    dir="$(dirname "$DESTINO/$f")"
    mkdir -p "$dir"
    cp "$CARGA/$f" "$DESTINO/$f.nimi-nuevo.$$"
    chmod 644 "$DESTINO/$f.nimi-nuevo.$$"
    mv -f "$DESTINO/$f.nimi-nuevo.$$" "$DESTINO/$f"
}

COPIADOS=0
# config/microapps.php se copia el último: mientras no se copie, la versión
# instalada sigue siendo 0.2.0 y el script puede repetirse sin problemas.
while IFS= read -r f; do
    [ -n "$f" ] || continue
    [ "$f" = "config/microapps.php" ] && continue
    copiar "$f"; COPIADOS=$((COPIADOS + 1))
done < <(cat "$MANIFIESTO/archivos-nuevos.txt" "$MANIFIESTO/archivos-modificados.txt")
copiar "config/microapps.php"; COPIADOS=$((COPIADOS + 1))
trap - ERR
info "$COPIADOS archivos copiados."
echo "Archivos copiados: $COPIADOS" >> "$REGISTRO"

# ---------------------------------------------------------------- 8. verificación

paso "8. Verificación"
( cd "$DESTINO" && cat "$MANIFIESTO/archivos-nuevos.txt" "$MANIFIESTO/archivos-modificados.txt" \
    | while IFS= read -r f; do printf '%s  %s\n' "$(huella_en "$MANIFIESTO/paquete.sha256" "$f")" "$f"; done \
    | sha256sum -c --quiet - ) \
    || error "algún archivo copiado no coincide con el paquete. Vuelve a ejecutar el script o restaura con $RESPALDO/restaurar-archivos.sh"
[ "$(leer_version "$DESTINO/config/microapps.php")" = "$VERSION_DESTINO" ] || error "la versión instalada no es $VERSION_DESTINO tras la copia."
info "Todos los archivos coinciden con el paquete. Versión de archivos: $VERSION_DESTINO."
echo "Verificación correcta" >> "$REGISTRO"

verde ""
verde "Archivos actualizados correctamente. La aplicación SIGUE en modo mantenimiento."
cat <<SIGUIENTE

Siguientes pasos (desde $DESTINO), según ACTUALIZACION-CPANEL-ENT-003.md:
  $PHP_BIN artisan config:clear
  $PHP_BIN artisan route:clear
  $PHP_BIN artisan view:clear
  $PHP_BIN artisan event:clear
  $PHP_BIN artisan migrate --force
  $PHP_BIN artisan optimize
  $PHP_BIN artisan microapps:check
  $PHP_BIN artisan up

Copia de los archivos sustituidos: $RESPALDO
SIGUIENTE
