#!/usr/bin/env bash
# =============================================================================
# add_fqdn_cert.sh — Issue a Let's Encrypt cert and deploy an Apache vhost pair
#                     for a new FQDN that proxies to the Togen gunicorn backend.
#
# USAGE
#   sudo /var/www/html/togen/scripts/add_fqdn_cert.sh <fqdn>
#
# ARGS
#   fqdn   Fully-qualified domain name to add (e.g. tenant.example.com).
#          Must resolve to this server's public IP (172.174.242.77) before
#          invocation; DNS must be live for ACME http-01 to succeed.
#
# EXIT CODES
#   0   success — cert issued (or already valid), vhosts enabled, Apache live
#   1   usage / validation error (bad argument, protected FQDN)
#   2   DNS validation failed (FQDN does not resolve to this server's IP)
#   3   certbot failed (cert could not be issued; see last_error)
#   4   Apache configtest failed (rendered vhost is syntactically invalid)
#   5   Apache reload failed
#
# STDOUT
#   Non-JSON progress lines are prefixed with "# " and safe to ignore.
#   The LAST line of stdout is always a single JSON object — the backend
#   (DVI-1026b) must parse ONLY the last line:
#
#   Success / already-active:
#     {"status":"active","fqdn":"<f>",
#      "cert":"/etc/letsencrypt/live/<f>/fullchain.pem",
#      "vhost_http":"/etc/apache2/sites-available/<f>.conf",
#      "vhost_ssl":"/etc/apache2/sites-available/<f>-ssl.conf"}
#
#   Error:
#     {"status":"error","fqdn":"<f>","last_error":"<human message>","exit_code":<n>}
#
# STATUS VALUES (for backend state machine)
#   active   cert issued, vhosts enabled, Apache serving traffic on this FQDN
#   error    terminal failure; inspect last_error and exit_code
#
# BACKEND CONTRACT (DVI-1026b)
#   • Invocation: sudo <script> <fqdn>    (manager has NOPASSWD sudo ALL)
#   • Parse the LAST stdout line as JSON.
#   • exit_code 0  → status is always "active"
#   • exit_code ≠0 → status is always "error"
#   • Safe to call multiple times (idempotent): if cert+vhosts already active,
#     exits 0 immediately without modifying anything.
#   • DNS must resolve before invocation; the script validates and fails fast
#     (exit 2) if the FQDN doesn't point here — surface this to the user as
#     "DNS not ready" before retrying.
#   • ACME webroot: /var/lib/letsencrypt/http_challenges
#     The HTTP vhost (written by this script) aliases /.well-known/ to that
#     directory. Apache is reloaded before certbot is invoked so the challenge
#     path is reachable.
#
# ADD-ONLY GUARANTEE
#   This script NEVER modifies the togen, docs, or fileshare core vhosts.
#   Each new FQDN gets its own isolated vhost pair in sites-available.
#
# ONE-TIME HOST SETUP (handled automatically on first run)
#   Installs /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh so the
#   existing certbot.timer reloads Apache after auto-renewal without manual
#   intervention.
# =============================================================================

set -uo pipefail

# ---- constants ---------------------------------------------------------------

SERVER_IP="172.174.242.77"
SITES_AVAILABLE="/etc/apache2/sites-available"
WEBROOT="/var/lib/letsencrypt/http_challenges"
GUNICORN_UPSTREAM="http://127.0.0.1:5001"
TOGEN_DOCROOT="/var/www/html/togen"
DEPLOY_HOOK="/etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh"
TS="$(date +%Y%m%d-%H%M%S)"

# Protected core vhosts — add_fqdn_cert.sh must never touch these
PROTECTED_FQDNS=("togen.icastinc.com" "docs.icastinc.com" "fileshare.icastinc.com")

# ---- helpers -----------------------------------------------------------------

FQDN="${1:-}"

log()  { printf '# %s\n' "$*" >&2; }
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$//' | tr -d '\n'; }

emit_success() {
    local cert="/etc/letsencrypt/live/${FQDN}/fullchain.pem"
    local http_conf="${SITES_AVAILABLE}/${FQDN}.conf"
    local ssl_conf="${SITES_AVAILABLE}/${FQDN}-ssl.conf"
    printf '{"status":"active","fqdn":"%s","cert":"%s","vhost_http":"%s","vhost_ssl":"%s"}\n' \
        "${FQDN}" "${cert}" "${http_conf}" "${ssl_conf}"
}

die() {
    local code="$1"; shift
    local msg
    msg="$(json_escape "$*")"
    printf '{"status":"error","fqdn":"%s","last_error":"%s","exit_code":%d}\n' \
        "${FQDN}" "${msg}" "${code}"
    exit "${code}"
}

bak_if_exists() {
    local f="$1"
    if [[ -f "${f}" ]]; then
        local bak="${f}.bak-fqdn-${TS}"
        log "Backing up ${f} -> ${bak}"
        cp -p "${f}" "${bak}"
    fi
}

run_configtest() {
    local context="$1"
    log "apachectl configtest (${context})..."
    local out
    if ! out="$(apachectl configtest 2>&1)"; then
        log "configtest output: ${out}"
        die 4 "Apache configtest failed (${context}): ${out}"
    fi
    log "configtest OK"
}

run_reload() {
    local context="$1"
    log "systemctl reload apache2 (${context})..."
    if ! systemctl reload apache2 2>&1; then
        die 5 "Apache reload failed (${context})"
    fi
    log "Apache reloaded OK"
}

# ---- one-time host setup: certbot deploy-hook --------------------------------

if [[ ! -f "${DEPLOY_HOOK}" ]]; then
    log "Installing certbot deploy-hook: ${DEPLOY_HOOK}"
    cat > "${DEPLOY_HOOK}" <<'HOOK'
#!/usr/bin/env bash
# Reload Apache after certbot auto-renews any cert so new TLS certs are served.
systemctl reload apache2
HOOK
    chmod 755 "${DEPLOY_HOOK}"
    log "Deploy hook installed."
else
    log "Deploy hook already present: ${DEPLOY_HOOK}"
fi

# ---- validate input ----------------------------------------------------------

if [[ -z "${FQDN}" ]]; then
    FQDN="(none)"
    die 1 "Usage: add_fqdn_cert.sh <fqdn>"
fi

# Basic FQDN sanity: labels of letters/digits/hyphens, at least two labels
if ! [[ "${FQDN}" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+$ ]]; then
    die 1 "Invalid FQDN: ${FQDN}"
fi

for protected in "${PROTECTED_FQDNS[@]}"; do
    if [[ "${FQDN}" == "${protected}" ]]; then
        die 1 "Protected FQDN ${FQDN}: this domain is managed by the core vhost — use the existing cert process"
    fi
done

HTTP_CONF="${SITES_AVAILABLE}/${FQDN}.conf"
SSL_CONF="${SITES_AVAILABLE}/${FQDN}-ssl.conf"
CERT_FULLCHAIN="/etc/letsencrypt/live/${FQDN}/fullchain.pem"
CERT_PRIVKEY="/etc/letsencrypt/live/${FQDN}/privkey.pem"

# ---- idempotency check -------------------------------------------------------

if [[ -f "${CERT_FULLCHAIN}" ]] \
    && [[ -e "/etc/apache2/sites-enabled/${FQDN}.conf" ]] \
    && [[ -e "/etc/apache2/sites-enabled/${FQDN}-ssl.conf" ]]; then
    log "Cert and vhosts already active for ${FQDN} — nothing to do"
    emit_success
    exit 0
fi

# ---- DNS validation ----------------------------------------------------------

log "Validating DNS for ${FQDN}..."
RESOLVED_IP="$(getent hosts "${FQDN}" 2>/dev/null | awk '{print $1; exit}')" || true
if [[ -z "${RESOLVED_IP}" ]]; then
    die 2 "DNS validation failed: ${FQDN} does not resolve — create an A record pointing to ${SERVER_IP} and retry"
fi
if [[ "${RESOLVED_IP}" != "${SERVER_IP}" ]]; then
    die 2 "DNS validation failed: ${FQDN} resolves to ${RESOLVED_IP}, expected ${SERVER_IP}"
fi
log "DNS OK: ${FQDN} -> ${RESOLVED_IP}"

# ---- step 1: HTTP vhost (serves /.well-known/ for ACME, redirects rest) -----

log "Writing HTTP vhost: ${HTTP_CONF}"
bak_if_exists "${HTTP_CONF}"

# Ensure the ACME challenge dir exists and is world-readable
mkdir -p "${WEBROOT}/.well-known/acme-challenge"
chmod 755 "${WEBROOT}/.well-known" "${WEBROOT}/.well-known/acme-challenge"

cat > "${HTTP_CONF}" <<VHOST
<VirtualHost *:80>
    ServerName ${FQDN}

    # Serve ACME http-01 challenges without redirecting to HTTPS.
    # certbot --webroot places tokens at ${WEBROOT}/.well-known/acme-challenge/
    Alias /.well-known/ ${WEBROOT}/.well-known/
    <Directory ${WEBROOT}/.well-known/>
        Require all granted
        Options None
    </Directory>

    RewriteEngine on
    RewriteCond %{REQUEST_URI} !^/.well-known/
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]

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

a2ensite "${FQDN}.conf" >/dev/null 2>&1

run_configtest "HTTP vhost"
run_reload     "HTTP vhost"

# ---- step 2: issue Let's Encrypt certificate ---------------------------------

log "Issuing Let's Encrypt certificate for ${FQDN} (webroot: ${WEBROOT})..."
CERTBOT_LOG="$(mktemp /tmp/certbot-XXXXXX.log)"
CERTBOT_RC=0
certbot certonly \
    --webroot \
    --webroot-path "${WEBROOT}" \
    --domain "${FQDN}" \
    --non-interactive \
    --agree-tos \
    2>&1 | tee "${CERTBOT_LOG}" >&2 || CERTBOT_RC=$?

if [[ "${CERTBOT_RC}" -ne 0 ]]; then
    CERTBOT_TAIL="$(tail -5 "${CERTBOT_LOG}" | tr '\n' ' ')"
    rm -f "${CERTBOT_LOG}"
    die 3 "certbot failed for ${FQDN} (exit ${CERTBOT_RC}): ${CERTBOT_TAIL}"
fi
rm -f "${CERTBOT_LOG}"
log "Certificate issued: /etc/letsencrypt/live/${FQDN}/"

# ---- step 3: HTTPS vhost (proxy to gunicorn, use new cert) ------------------

log "Writing HTTPS vhost: ${SSL_CONF}"
bak_if_exists "${SSL_CONF}"

cat > "${SSL_CONF}" <<VHOST
<IfModule mod_ssl.c>
<VirtualHost *:443>
    ServerName ${FQDN}
    DocumentRoot ${TOGEN_DOCROOT}

    # Serve static assets directly with long-lived immutable caching
    Alias /static ${TOGEN_DOCROOT}/static
    <Directory ${TOGEN_DOCROOT}/static>
        Require all granted
        Options -Indexes
        Header set Cache-Control "public, max-age=31536000, immutable"
    </Directory>

    ProxyPreserveHost On
    RequestHeader set X-Forwarded-Proto "https"
    ProxyPass /static !
    ProxyPass / ${GUNICORN_UPSTREAM}/
    ProxyPassReverse / ${GUNICORN_UPSTREAM}/

    ErrorLog \${APACHE_LOG_DIR}/${FQDN}-error.log
    CustomLog \${APACHE_LOG_DIR}/${FQDN}-access.log combined

    SSLCertificateFile ${CERT_FULLCHAIN}
    SSLCertificateKeyFile ${CERT_PRIVKEY}
    Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>
VHOST

a2ensite "${FQDN}-ssl.conf" >/dev/null 2>&1

run_configtest "SSL vhost"
run_reload     "SSL vhost"

# ---- success -----------------------------------------------------------------

log "FQDN ${FQDN} is now active and proxied to Togen."
emit_success
exit 0
