#!/usr/bin/env bash

# Despliega Gizmedic (Next.js, en Docker) desde /var/gizmedic/html en Debian/Ubuntu.
#
# Primera ejecucion:
#   sudo bash /var/gizmedic/html/deploy.sh --email admin@gizmedic.com
#
# Ejecuciones posteriores:
#   sudo bash /var/gizmedic/html/deploy.sh
#
# Para actualizar el repositorio antes de desplegar (solo si esta limpio):
#   sudo bash /var/gizmedic/html/deploy.sh --pull
#
# Si /var/gizmedic/html/.env existe, sus variables se pasan al contenedor
# en runtime (opcional; la app no requiere ninguna por ahora).

set -Eeuo pipefail
umask 0027
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

DOMAIN="gizmedic.com"
APP_NAME="gizmedic"
SOURCE_DIR="/var/gizmedic/html"
IMAGE_NAME="${APP_NAME}"
CONTAINER_NAME="${APP_NAME}"
STATE_DIR="/var/lib/${APP_NAME}"
PORT_FILE="${STATE_DIR}/port"
CURRENT_IMAGE_FILE="${STATE_DIR}/current_image"
VHOST_FILE="/etc/apache2/sites-available/${DOMAIN}.conf"
SSL_VHOST_FILE="/etc/apache2/sites-available/${DOMAIN}-ssl.conf"
ACME_ROOT="/var/www/certbot"
CERT_LIVE_DIR="/etc/letsencrypt/live/${DOMAIN}"
PORT_MIN=3000
PORT_MAX=3999
CERTBOT_EMAIL="${CERTBOT_EMAIL:-}"
AUTO_PULL=0
SKIP_CERTBOT=0
IMAGE_TAG=""
PREVIOUS_IMAGE_TAG=""

log() {
  printf '\n[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}

fail() {
  printf '\nERROR: %s\n' "$*" >&2
  exit 1
}

usage() {
  cat <<'EOF'
Uso:
  sudo bash deploy.sh [opciones]

Opciones:
  --email EMAIL    Email de Let's Encrypt (obligatorio al crear el certificado).
  --pull           Ejecuta git pull --ff-only antes del despliegue.
  --skip-certbot   Publica solamente por HTTP; no solicita/configura HTTPS.
  -h, --help       Muestra esta ayuda.

Variables opcionales:
  CERTBOT_EMAIL    Alternativa a --email.
EOF
}

while (($# > 0)); do
  case "$1" in
    --email)
      (($# >= 2)) || fail "Falta el valor de --email."
      CERTBOT_EMAIL="$2"
      shift 2
      ;;
    --pull)
      AUTO_PULL=1
      shift
      ;;
    --skip-certbot)
      SKIP_CERTBOT=1
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      fail "Opcion desconocida: $1"
      ;;
  esac
done

[[ ${EUID} -eq 0 ]] || fail "Ejecuta este script con sudo o como root."
[[ -d "${SOURCE_DIR}" ]] || fail "No existe el repositorio ${SOURCE_DIR}."
[[ -f "${SOURCE_DIR}/package.json" ]] || fail "No se encontro package.json en ${SOURCE_DIR}."
[[ -f "${SOURCE_DIR}/package-lock.json" ]] || fail "El build de Docker requiere ${SOURCE_DIR}/package-lock.json."
[[ -f "${SOURCE_DIR}/Dockerfile" ]] || fail "No se encontro Dockerfile en ${SOURCE_DIR}."

# Opcional: si existe, se pasa al contenedor como variables de entorno
# de runtime (la app no requiere ninguna por ahora).
ENV_FILE="${SOURCE_DIR}/.env"

if [[ ! -f /etc/os-release ]]; then
  fail "No se pudo identificar el sistema operativo. Este script soporta Debian/Ubuntu."
fi

# shellcheck disable=SC1091
source /etc/os-release
case "${ID:-}" in
  debian|ubuntu) ;;
  *) fail "Sistema no soportado (${ID:-desconocido}). Este script esta preparado para Debian/Ubuntu." ;;
esac

if [[ ${SKIP_CERTBOT} -eq 0 && ! -f "${CERT_LIVE_DIR}/fullchain.pem" && -z "${CERTBOT_EMAIL}" ]]; then
  fail "El primer certificado requiere un email. Usa --email tu-email@dominio.com."
fi

log "Instalando dependencias del sistema"
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y apache2 curl iproute2 ca-certificates
id www-data >/dev/null 2>&1 || fail "Apache no creo el usuario www-data."

if command -v ufw >/dev/null 2>&1 && ufw status | grep -Eqi '^Status:[[:space:]]+(active|activo)'; then
  log "Habilitando HTTP y HTTPS en UFW"
  ufw allow 'Apache Full' >/dev/null
fi

docker_is_ready() {
  command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1
}

install_docker() {
  log "Instalando Docker Engine"
  install -m 0755 -d /etc/apt/keyrings
  curl --fail --silent --show-error --location \
    "https://download.docker.com/linux/${ID}/gpg" \
    -o /etc/apt/keyrings/docker.asc
  chmod a+r /etc/apt/keyrings/docker.asc

  local arch
  arch="$(dpkg --print-architecture)"
  cat > /etc/apt/sources.list.d/docker.list <<EOF
deb [arch=${arch} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable
EOF

  apt-get update
  apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  systemctl enable --now docker
}

if ! docker_is_ready; then
  install_docker
fi
docker_is_ready || fail "No se pudo instalar o iniciar Docker."
log "Docker detectado: $(docker --version)"

if [[ ${SKIP_CERTBOT} -eq 0 ]]; then
  if [[ -x /snap/bin/certbot ]]; then
    ln -sfn /snap/bin/certbot /usr/local/bin/certbot
    hash -r
  else
    log "Instalando Certbot actualizado mediante Snap"
    apt-get install -y snapd
    systemctl enable --now snapd.socket >/dev/null

    # La documentacion oficial recomienda no mezclar el paquete APT con el Snap.
    if dpkg-query -W -f='${Status}' certbot 2>/dev/null | grep -q 'ok installed'; then
      apt-get remove -y certbot
    fi

    timeout 120 snap wait system seed.loaded >/dev/null 2>&1 || true
    if ! snap list core >/dev/null 2>&1; then
      snap install core
    else
      snap refresh core || true
    fi
    snap install --classic certbot
    ln -sfn /snap/bin/certbot /usr/local/bin/certbot
    hash -r
  fi
  command -v certbot >/dev/null 2>&1 || fail "No se pudo instalar Certbot."
  log "Certbot detectado: $(certbot --version 2>&1)"
fi

if [[ ${AUTO_PULL} -eq 1 ]]; then
  command -v git >/dev/null 2>&1 || fail "--pull requiere git."
  [[ -d "${SOURCE_DIR}/.git" ]] || fail "${SOURCE_DIR} no es un repositorio Git."
  [[ -z "$(git -C "${SOURCE_DIR}" status --porcelain)" ]] || fail "El repositorio tiene cambios locales; no se ejecutara git pull."
  log "Actualizando el repositorio"
  git -C "${SOURCE_DIR}" pull --ff-only
fi

mkdir -p "${STATE_DIR}" "${ACME_ROOT}/.well-known/acme-challenge"

if [[ ! -f "${SOURCE_DIR}/.env.production" ]]; then
  printf 'NEXT_PUBLIC_DESIGN_VARIANT="nova"\n' > "${SOURCE_DIR}/.env.production"
fi

port_is_free() {
  local candidate="$1"
  ! ss -H -ltn | awk '{print $4}' | grep -Eq "(^|:)${candidate}$"
}

container_is_running() {
  [[ "$(docker inspect --format '{{.State.Running}}' "${CONTAINER_NAME}" 2>/dev/null)" == "true" ]]
}

choose_port() {
  local saved=""
  local candidate

  if [[ -f "${PORT_FILE}" ]]; then
    saved="$(tr -dc '0-9' < "${PORT_FILE}")"
  fi

  if [[ "${saved}" =~ ^[0-9]+$ ]] && ((saved >= PORT_MIN && saved <= PORT_MAX)); then
    if container_is_running || port_is_free "${saved}"; then
      printf '%s' "${saved}"
      return 0
    fi
    log "El puerto guardado ${saved} esta ocupado por otro proceso; se buscara otro" >&2
  fi

  for ((candidate = PORT_MIN; candidate <= PORT_MAX; candidate++)); do
    if port_is_free "${candidate}"; then
      printf '%s' "${candidate}"
      return 0
    fi
  done

  return 1
}

PORT="$(choose_port)" || fail "No hay puertos libres entre ${PORT_MIN} y ${PORT_MAX}."
printf '%s\n' "${PORT}" > "${PORT_FILE}"
chmod 0640 "${PORT_FILE}"

RELEASE_ID="$(date '+%Y%m%d%H%M%S')-$$"
IMAGE_TAG="${IMAGE_NAME}:${RELEASE_ID}"

if [[ -f "${CURRENT_IMAGE_FILE}" ]]; then
  PREVIOUS_IMAGE_TAG="$(<"${CURRENT_IMAGE_FILE}")"
fi

log "Construyendo la imagen Docker ${IMAGE_TAG}"
docker build --pull -t "${IMAGE_TAG}" -t "${IMAGE_NAME}:latest" "${SOURCE_DIR}"

start_container() {
  local tag="$1"
  local env_file_args=()
  [[ -f "${ENV_FILE}" ]] && env_file_args=(--env-file "${ENV_FILE}")
  docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
  docker run -d \
    --name "${CONTAINER_NAME}" \
    --restart unless-stopped \
    "${env_file_args[@]}" \
    -p "127.0.0.1:${PORT}:3000" \
    "${tag}" >/dev/null
}

rollback_container() {
  if [[ -n "${PREVIOUS_IMAGE_TAG}" ]] && docker image inspect "${PREVIOUS_IMAGE_TAG}" >/dev/null 2>&1; then
    log "El contenedor nuevo fallo; restaurando ${PREVIOUS_IMAGE_TAG}"
    start_container "${PREVIOUS_IMAGE_TAG}"
  fi
}

log "Iniciando el contenedor ${CONTAINER_NAME} con la imagen ${IMAGE_TAG} en el puerto ${PORT}"
start_container "${IMAGE_TAG}"

APP_READY=0
for _ in {1..30}; do
  if curl --fail --silent --max-time 2 "http://127.0.0.1:${PORT}/" >/dev/null; then
    APP_READY=1
    break
  fi
  container_is_running || break
  sleep 1
done

if [[ ${APP_READY} -ne 1 ]]; then
  printf '\n--- docker logs %s (ultimas 100 lineas) ---\n' "${CONTAINER_NAME}" >&2
  docker logs --tail 100 "${CONTAINER_NAME}" >&2 || true
  rollback_container
  fail "La app no respondio en 127.0.0.1:${PORT}. Revisa: docker logs ${CONTAINER_NAME}"
fi

printf '%s\n' "${IMAGE_TAG}" > "${CURRENT_IMAGE_FILE}"

log "Eliminando imagenes antiguas de ${IMAGE_NAME}"
mapfile -t OLD_IMAGES < <(docker images "${IMAGE_NAME}" --format '{{.Tag}}' | grep -E '^[0-9]{14}-[0-9]+$' | sort -r | tail -n +4)
for old_tag in "${OLD_IMAGES[@]}"; do
  docker rmi "${IMAGE_NAME}:${old_tag}" >/dev/null 2>&1 || true
done

log "Configurando Apache como proxy inverso"
a2enmod proxy proxy_http ssl rewrite >/dev/null

BACKUP_DIR="/var/backups/${APP_NAME}-apache/${RELEASE_ID}"
mkdir -p "${BACKUP_DIR}"
shopt -s nullglob
for enabled_site in /etc/apache2/sites-enabled/*.conf; do
  site_name="$(basename "${enabled_site}")"
  if [[ "${site_name}" == "$(basename "${VHOST_FILE}")" || "${site_name}" == "$(basename "${SSL_VHOST_FILE}")" ]]; then
    continue
  fi
  if grep -Eiq "^[[:space:]]*ServerName[[:space:]]+${DOMAIN//./\\.}([[:space:]]|$)" "${enabled_site}"; then
    cp -aL "${enabled_site}" "${BACKUP_DIR}/${site_name}"
    a2dissite "${site_name}" >/dev/null
  fi
done
shopt -u nullglob

cat > "${VHOST_FILE}" <<EOF
<VirtualHost *:80>
    ServerName ${DOMAIN}

    ProxyRequests Off
    ProxyPreserveHost On

    ProxyPass /.well-known/acme-challenge/ !
    Alias /.well-known/acme-challenge/ ${ACME_ROOT}/.well-known/acme-challenge/
    <Directory "${ACME_ROOT}/.well-known/acme-challenge/">
        Options None
        AllowOverride None
        Require all granted
    </Directory>

    ProxyPass        / http://127.0.0.1:${PORT}/ retry=0 timeout=60
    ProxyPassReverse / http://127.0.0.1:${PORT}/

    ErrorLog \${APACHE_LOG_DIR}/${APP_NAME}-error.log
    CustomLog \${APACHE_LOG_DIR}/${APP_NAME}-access.log combined
</VirtualHost>
EOF

a2ensite "$(basename "${VHOST_FILE}")" >/dev/null

# Deshabilita los sitios por defecto de Apache/Ubuntu de forma incondicional:
# si no se hace esto, un Host header que no matchee ningun ServerName (p.ej.
# entrar por IP, o un intento previo de deploy que corto antes de este punto)
# termina sirviendo la pagina de bienvenida en vez de la app.
for default_site in 000-default.conf default-ssl.conf; do
  if [[ -L "/etc/apache2/sites-enabled/${default_site}" ]]; then
    a2dissite "${default_site}" >/dev/null
  fi
done

apachectl configtest
systemctl reload apache2

if [[ ${SKIP_CERTBOT} -eq 0 ]]; then
  log "Solicitando o comprobando el certificado de ${DOMAIN}"
  CERTBOT_ARGS=(
    certonly
    --webroot
    --webroot-path "${ACME_ROOT}"
    --cert-name "${DOMAIN}"
    --domain "${DOMAIN}"
    --non-interactive
    --agree-tos
    --keep-until-expiring
  )
  if [[ -n "${CERTBOT_EMAIL}" ]]; then
    CERTBOT_ARGS+=(--email "${CERTBOT_EMAIL}")
  fi
  certbot "${CERTBOT_ARGS[@]}"

  [[ -f "${CERT_LIVE_DIR}/fullchain.pem" ]] || fail "Certbot termino sin crear ${CERT_LIVE_DIR}/fullchain.pem."
  [[ -f "${CERT_LIVE_DIR}/privkey.pem" ]] || fail "Certbot termino sin crear ${CERT_LIVE_DIR}/privkey.pem."

  cat > "${SSL_VHOST_FILE}" <<EOF
<IfModule mod_ssl.c>
<VirtualHost *:443>
    ServerName ${DOMAIN}

    ProxyRequests Off
    ProxyPreserveHost On
    ProxyPass        / http://127.0.0.1:${PORT}/ retry=0 timeout=60
    ProxyPassReverse / http://127.0.0.1:${PORT}/

    SSLEngine on
    SSLCertificateFile ${CERT_LIVE_DIR}/fullchain.pem
    SSLCertificateKeyFile ${CERT_LIVE_DIR}/privkey.pem

    ErrorLog \${APACHE_LOG_DIR}/${APP_NAME}-ssl-error.log
    CustomLog \${APACHE_LOG_DIR}/${APP_NAME}-ssl-access.log combined
</VirtualHost>
</IfModule>
EOF

  # Una vez disponible HTTPS, el VirtualHost de HTTP conserva el challenge
  # de Let's Encrypt y redirige el resto del trafico.
  cat > "${VHOST_FILE}" <<EOF
<VirtualHost *:80>
    ServerName ${DOMAIN}

    ProxyPass /.well-known/acme-challenge/ !
    Alias /.well-known/acme-challenge/ ${ACME_ROOT}/.well-known/acme-challenge/
    <Directory "${ACME_ROOT}/.well-known/acme-challenge/">
        Options None
        AllowOverride None
        Require all granted
    </Directory>

    RewriteEngine On
    RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
    RewriteRule ^ https://${DOMAIN}%{REQUEST_URI} [R=301,L,NE]

    ErrorLog \${APACHE_LOG_DIR}/${APP_NAME}-error.log
    CustomLog \${APACHE_LOG_DIR}/${APP_NAME}-access.log combined
</VirtualHost>
EOF

  a2ensite "$(basename "${SSL_VHOST_FILE}")" >/dev/null

  install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
  cat > /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh <<'EOF'
#!/usr/bin/env bash
set -e
apachectl configtest
systemctl reload apache2
EOF
  chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh

  if systemctl list-unit-files certbot.timer >/dev/null 2>&1; then
    systemctl enable --now certbot.timer >/dev/null
  fi

  apachectl configtest
  systemctl reload apache2
fi

log "Despliegue completado"
printf 'Imagen:      %s\n' "${IMAGE_TAG}"
printf 'Contenedor:  docker ps --filter name=%s\n' "${CONTAINER_NAME}"
printf 'Logs:        docker logs -f %s\n' "${CONTAINER_NAME}"
printf 'Puerto:      127.0.0.1:%s\n' "${PORT}"
if [[ ${SKIP_CERTBOT} -eq 0 ]]; then
  printf 'Web:         https://%s\n' "${DOMAIN}"
else
  printf 'Web:         http://%s\n' "${DOMAIN}"
fi
