#!/bin/bash
# ==============================================================================================
# ============================= Conf Cache Sync ================================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Maintains a RAM-resident conf cache at /tmp/.cache/vv/d/.
# Credentials and partner keys live in RAM only — never on disk across hosts.
#
# On array start (default / --array-start):
#   1. Copy own conf to local cache
#   2. Pull each available partner's conf from their disk → local cache
#   3. Push own conf to each available partner's /tmp/.cache/vv/d/ cache
#
# On conf save (--push-only):
#   Fast path — push updated own conf to all partners' /tmp/.cache/vv/d/ only.
#   No pulls, no local cache rebuild.
#
# Cache is /tmp/.cache/vv/d (tmpfs) — cleared every reboot, repopulated by
# this script on next array start. Scripts source from cache for partner vars;
# own vars always come from disk (load_config.sh skips cached copy of own conf).
#
# Pull path resolves the remote's SCRIPTS_DIR from their varaverk.cfg so it
# works whether the remote is in internal or appdata storage mode.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
#   1. Gates — PARTNERSHIP_ENABLED, CONF_SYNC_ENABLED
#   2. Cache own conf into the local RAM cache (skipped in --push-only / --pull-only)
#   3. Per partner:
#        a. Resolve the partner's own SCRIPTS_DIR by reading their varaverk.cfg over SSH,
#           so a partner in appdata storage mode is still found
#        b. Pull  — scp their host*.conf from their disk into our RAM cache
#        c. Push  — scp our host*.conf into their RAM cache
#      A partner that fails SSH is counted and skipped; the others still sync.
#
# Every file written locally or remotely is restricted to 600, in a 700 directory.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# RAM-Only Credentials
#   Partner credentials and keys never touch disk on the receiving server.
#   /tmp/.cache/vv/d is tmpfs — cleared every reboot. This is intentional:
#   partner conf files are not discoverable on disk between boots.
#
# Pull Resolves Remote Path
#   The pull step reads the remote's varaverk.cfg to discover their SCRIPTS_DIR
#   before SSHing for the conf. Works whether the remote is in internal or
#   appdata storage mode — no hardcoded path assumptions about the remote.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Enforcement
#   Reads the on-disk conf and writes the RAM cache; SSH/scp run as root.
#
# Lock Acquisition
#   acquire_lock prevents concurrent sync runs writing the same cache files.
#
# Partnership Gate
#   require_partnership exits early if PARTNERSHIP_ENABLED=false.
#
# CONF_SYNC_ENABLED Gate
#   Exits cleanly when disabled, without removing it from the schedule.
#
# Host Detection
#   detect_hosts() builds the partner list used for push/pull routing.
#
# SSH Reachability
#   Partners that fail SSH are counted and skipped, never fatal — one unreachable
#   partner does not prevent the others from syncing.
#
# SSH Timeouts
#   Every ssh and scp call is wrapped in timeout with ConnectTimeout and BatchMode,
#   so an unresponsive or password-prompting partner cannot stall the run.
#
# Remote Path Discovery
#   The partner's SCRIPTS_DIR is read from their own varaverk.cfg rather than assumed,
#   so a partner in appdata storage mode is still found. Falls back to the default
#   plugin path if the file cannot be read.
#
# Credential File Permissions
#   Cache directories are created 700 and every conf written 600 — on both ends. These
#   files carry NPM/lldap passwords and API keys, and the cache lives under a
#   world-readable /tmp path. The pushed copy is chmod'd on the partner too, since our
#   own credentials land on their disk.
#
# Dry Run Support
#   --dry-run reports every pull and push without transferring anything.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
#   CONF_SYNC_ENABLED
#       Master toggle for conf syncing (default: true)
#
#   CONF_RAM_CACHE_DIR
#       tmpfs cache both ends read partner vars from (/tmp/.cache/vv/d). Cleared every
#       reboot, which is why conf_cache_save.sh / conf_cache_restore.sh exist.
#
#   SSH_KEY
#       Key used for all partner ssh/scp operations
#
#   PARTNERSHIP_ENABLED
#       Checked via require_partnership()
#
# host*.conf
#
#   HOST*  — hostnames used to build the partner list via detect_hosts()
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
#   conf_sync.sh                   Full sync: pull from all partners + push to all partners
#   conf_sync.sh --push-only       Push own conf to all partners (fast, for conf-save hook)
#   conf_sync.sh --pull-only       Pull partner confs into local cache only (for intermediate orch)
#   conf_sync.sh --dry-run         Show what would happen, no changes
#   conf_sync.sh --log             Verbose output
#
# ==============================================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"

PUSH_ONLY=false
PULL_ONLY=false
FILTERED_ARGS=()
for arg in "$@"; do
    case "$arg" in
        --push-only) PUSH_ONLY=true ;;
        --pull-only) PULL_ONLY=true ;;
        *)           FILTERED_ARGS+=("$arg") ;;
    esac
done
parse_args "${FILTERED_ARGS[@]}"

if [[ "$EUID" -ne 0 ]]; then
    error "Must be run as root"
    exit 1
fi

acquire_lock

detect_hosts
require_partnership

if [[ "${CONF_SYNC_ENABLED:-true}" == false ]]; then
    log "CONF_SYNC_ENABLED=false — skipping"
    exit 0
fi

CACHE_DIR="$CONF_RAM_CACHE_DIR"
MY_CONF="$SCRIPTS_DIR/Configurations/${MY_ID,,}.conf"
SSH_TIMEOUT=10

# Reads the remote's varaverk.cfg to find their actual SCRIPTS_DIR.
# Handles the case where the remote is in appdata storage mode.
_remote_scripts_dir() {
    local ip="$1"
    local cfg line sd
    cfg=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
        "root@${ip}" "cat /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null" 2>/dev/null) || true
    while IFS= read -r line; do
        [[ "$line" == SCRIPTS_DIR=* ]] || continue
        sd="${line#SCRIPTS_DIR=}"; sd="${sd//\"/}"; sd="${sd//\'/}"
        echo "$sd"; return
    done <<< "$cfg"
    echo "/boot/config/plugins/varaverk"
}

[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"

# ── Ensure cache dir exists ───────────────────────────────────────────────────
if [[ "$DRY_RUN" == false ]]; then
    # These conf files carry credentials (NPM/lldap passwords, API keys). The cache lives in
    # a world-readable /tmp path, so the directory and every file written into it below are
    # restricted explicitly rather than left at the default umask.
    mkdir -p "$CACHE_DIR" && chmod 700 "$CACHE_DIR"
fi

# ── Copy own conf into local cache ───────────────────────────────────────────
if [[ "$PUSH_ONLY" == false ]] && [[ "$PULL_ONLY" == false ]]; then
    if [[ -f "$MY_CONF" ]]; then
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would copy $(basename "$MY_CONF") → $CACHE_DIR/"
        else
            if cp "$MY_CONF" "$CACHE_DIR/${MY_ID,,}.conf" && \
               chmod 600 "$CACHE_DIR/${MY_ID,,}.conf"; then
                echo "Own conf cached ✅"
            else
                warn "Failed to cache own conf"
            fi
        fi
    else
        warn "Own conf not found: $MY_CONF"
    fi
fi

# ── Per-partner sync ──────────────────────────────────────────────────────────
PUSHED=0
PULLED=0
FAILED=0

for host_var in $(compgen -v | grep -E '^HOST[0-9]+$' | sort); do
    partner_host="${!host_var}"
    [[ -z "$partner_host" ]] && continue
    [[ "${host_var,,}" == "${MY_ID,,}" ]] && continue

    partner_slot="${host_var,,}"   # e.g. host2
    partner_ip=$(resolve_tailscale_ip "$partner_host" 2>/dev/null || true)

    if [[ -z "$partner_ip" ]]; then
        warn "$partner_host — cannot resolve Tailscale IP, skipping"
        (( FAILED++ ))
        continue
    fi

    # ── Pull: grab partner's conf from their disk → our local cache ──────────
    if [[ "$PUSH_ONLY" == false ]]; then
        remote_sd=$(_remote_scripts_dir "$partner_ip")
        remote_conf="${remote_sd}/Configurations/${partner_slot}.conf"
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would pull $partner_host:$remote_conf → $CACHE_DIR/${partner_slot}.conf"
        elif timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
            "root@${partner_ip}:${remote_conf}" \
            "$CACHE_DIR/${partner_slot}.conf" 2>/dev/null; then
            chmod 600 "$CACHE_DIR/${partner_slot}.conf" 2>/dev/null
            echo "Pulled ${partner_slot}.conf from $partner_host ✅"
            (( PULLED++ ))
        else
            warn "Could not pull ${partner_slot}.conf from $partner_host"
            (( FAILED++ ))
        fi
    fi

    # ── Push: send own conf to partner's /tmp/.cache/vv/d/ cache ───────────
    if [[ "$PULL_ONLY" == true ]]; then
        continue
    fi

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would push ${MY_ID,,}.conf → $partner_host:/tmp/.cache/vv/d/"
        continue
    fi

    # Ensure partner's cache dir exists, then SCP own conf into it
    timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
        "root@${partner_ip}" "mkdir -p '$CACHE_DIR' && chmod 700 '$CACHE_DIR'" 2>/dev/null

    if timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \
        -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
        "$MY_CONF" \
        "root@${partner_ip}:${CACHE_DIR}/${MY_ID,,}.conf" 2>/dev/null; then
        # Our own conf lands on the partner carrying our credentials — restrict it there too.
        timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
            -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
            "root@${partner_ip}" "chmod 600 '${CACHE_DIR}/${MY_ID,,}.conf'" 2>/dev/null
        echo "Pushed ${MY_ID,,}.conf to $partner_host ✅"
        (( PUSHED++ ))
    else
        warn "Could not push to $partner_host"
        (( FAILED++ ))
    fi
done

# ── Summary ───────────────────────────────────────────────────────────────────
if [[ "$PUSH_ONLY" == true ]]; then
    info "Conf push complete — pushed to $PUSHED host(s)${FAILED:+, $FAILED failed}"
elif [[ "$PULL_ONLY" == true ]]; then
    info "Conf pull complete — pulled $PULLED partner conf(s)${FAILED:+, $FAILED failed}"
else
    info "Conf sync complete — pulled $PULLED, pushed $PUSHED${FAILED:+, $FAILED failed}"
fi

if [[ "$FAILED" -gt 0 ]]; then
    notify "Conf sync on $LOCAL_SERVER_NAME ($MY_ID) — $FAILED partner(s) failed. Partner config cache may be stale." \
        "Conf Sync" "warning"
    exit 1
fi

exit 0
