#!/usr/bin/env bash
# Reset SSL renewal state for haproxy_domains rows previously blocked by the
# pre-flight DNS check when the resolved IP was a Cloudflare edge.
#
# Background: before 2026.06.x, ssl_renewal_orchestrator::checkDomainReachability()
# treated "domain resolves to CF edge IP" as DNS_UNREACHABLE and never attempted
# ACME. That blocked SSL for the entire cpanel-imported CF estate of customers
# who front their sites with Cloudflare. The orchestrator now whitelists CF
# ranges and lets ACME run — but rows already marked unreachable stay marked
# unreachable until manually reset or until UNREACHABLE_GIVEUP_DAYS elapses.
#
# This script resets those rows so the next orchestrator sweep retries them.
# Idempotent: only touches rows whose last_renewal_error mentions DNS_UNREACHABLE.
# Safe to re-run.

set -euo pipefail

LOG_PREFIX="[reset-cf-blocked-ssl-states]"

log() { echo "$LOG_PREFIX $*"; }

if ! command -v mysql >/dev/null 2>&1; then
    log "mysql CLI not found; skipping."
    exit 0
fi

# Don't even try if the table doesn't exist yet (fresh install).
if ! mysql whp -BN -e "SHOW TABLES LIKE 'haproxy_domains'" 2>/dev/null | grep -q haproxy_domains; then
    log "haproxy_domains table not present; skipping."
    exit 0
fi

# Resolve each unreachable domain. If the A record falls into a Cloudflare
# range, reset renewal_state so the orchestrator retries it. We don't
# bother with the AWS / GCP / generic case — only CF was previously broken.
#
# Inline-test CF range as a CIDR list. Match the orchestrator's list verbatim
# so we don't drift.
CF_RANGES=(
    "173.245.48.0/20"
    "103.21.244.0/22"
    "103.22.200.0/22"
    "103.31.4.0/22"
    "141.101.64.0/18"
    "108.162.192.0/18"
    "190.93.240.0/20"
    "188.114.96.0/20"
    "197.234.240.0/22"
    "198.41.128.0/17"
    "162.158.0.0/15"
    "104.16.0.0/13"
    "104.24.0.0/14"
    "172.64.0.0/13"
    "131.0.72.0/22"
)

# Read every domain currently marked unreachable due to the pre-flight gate.
mapfile -t DOMAINS < <(mysql whp -BN -e "
    SELECT domain_name
    FROM haproxy_domains
    WHERE renewal_state = 'unreachable'
      AND last_renewal_error LIKE '%DNS_UNREACHABLE%'
      AND is_wildcard = 0
" 2>/dev/null)

if [[ ${#DOMAINS[@]} -eq 0 ]]; then
    log "no DNS_UNREACHABLE rows to evaluate; nothing to do."
    exit 0
fi

log "evaluating ${#DOMAINS[@]} domain(s) currently in unreachable state"

# IP-in-CIDR test in bash. Returns 0 if $1 falls within any CF range.
ip_to_int() {
    local IFS=.
    read -r a b c d <<< "$1"
    echo $(( (a << 24) | (b << 16) | (c << 8) | d ))
}

is_cf_ip() {
    local ip="$1"
    if ! [[ $ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then return 1; fi
    local ip_int
    ip_int=$(ip_to_int "$ip")
    for cidr in "${CF_RANGES[@]}"; do
        local subnet="${cidr%/*}"
        local mask="${cidr#*/}"
        local subnet_int
        subnet_int=$(ip_to_int "$subnet")
        local mask_int=$(( 0xFFFFFFFF << (32 - mask) & 0xFFFFFFFF ))
        if (( (ip_int & mask_int) == (subnet_int & mask_int) )); then
            return 0
        fi
    done
    return 1
}

reset_count=0
skipped_count=0

for domain in "${DOMAINS[@]}"; do
    # `getent hosts` returns IPv6 first when available, which breaks the
    # IPv4 CIDR check. PHP's gethostbyname (used by the orchestrator) is
    # always IPv4 — match that behaviour explicitly with ahostsv4.
    #
    # `getent ahostsv4` exits 2 on NXDOMAIN, which under `set -euo pipefail`
    # would kill the whole script mid-loop (this bit us on whp01 2026-06-01
    # — the script reset 7 rows then aborted when it hit the first NXDOMAIN,
    # which then propagated as update.sh exit-1 and the version bump never
    # happened). `|| true` swallows getent's non-zero so the loop continues
    # and the empty-resolved branch below counts it as skipped.
    resolved=$(getent ahostsv4 "$domain" 2>/dev/null | awk '/STREAM/{print $1; exit}' || true)
    if [[ -z "$resolved" ]]; then
        skipped_count=$((skipped_count + 1))
        continue
    fi
    if is_cf_ip "$resolved"; then
        # Use prepared-style escaping via printf %q to keep the domain literal
        # safe; haproxy_domains.domain_name is char-only so this is belt+brace.
        mysql whp -e "
            UPDATE haproxy_domains
            SET renewal_state = 'pending',
                consecutive_failures = 0,
                last_renewal_error = NULL,
                next_renewal_attempt_at = NULL
            WHERE domain_name = '$(printf '%s' "$domain" | sed "s/'/''/g")'
              AND renewal_state = 'unreachable'
        " 2>/dev/null && {
            log "reset $domain (was unreachable, resolves to $resolved [Cloudflare])"
            reset_count=$((reset_count + 1))
        }
    else
        skipped_count=$((skipped_count + 1))
    fi
done

log "done: reset=$reset_count, skipped=$skipped_count"
log "next orchestrator sweep will retry the reset domains via HTTP-01"
