#!/bin/bash
# ==============================================================================================
# ============================= conf_upgrade.sh ================================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Merges a new conf template into an existing user conf while preserving every
# value the user has already set. Run manually when the conf schema changes
# between versions — adds new keys, removes deprecated ones, and keeps the
# structure of the new template exactly.
#
#   Keys in template only  → ADDED   (placeholder/default — user fills in once)
#   Keys in target only    → REMOVED (deprecated in new version)
#   Keys in both           → KEPT    (target's value always wins, template ignored)
#   Comments / blank lines → always from template (structure follows new version)
#
# Supports all conf variable patterns: simple scalars, indexed arrays, and
# associative arrays (declare -A).
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Dry-run Mode
#   --dry-run prints the full change report (ADDED / REMOVED / KEPT) then exits
#   without writing anything. Always preview before applying to production confs.
#
# Backup Option
#   --backup writes a .bak copy of the target before overwriting. Use when
#   applying to a conf that has never been upgraded before.
#
# File Existence Guards
#   Both --template and --target are validated before any parsing begins.
#   Missing files abort immediately with a clear error.
#
# Atomic Write
#   Merged output is written to a tempfile first, then copied to the target.
#   A partial write cannot corrupt the original.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# No conf vars. All inputs are CLI flags.
#
#   --template <file>   New version conf file (source of structure and defaults)
#   --target   <file>   Existing user conf (source of real values — always preserved)
#   --dry-run           Show what would change without writing
#   --backup            Write a .bak copy of target before modifying
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# conf_upgrade.sh --template master.conf.template --target Configurations/master.conf --dry-run
#     Preview what would be added, removed, and kept — no changes written.
#
# conf_upgrade.sh --template master.conf.template --target Configurations/master.conf --backup
#     Apply the upgrade, writing a .bak first.
#
# conf_upgrade.sh --template master.conf.template --target Configurations/master.conf
#     Apply the upgrade in-place with no backup.
#
# ==============================================================================================

set -uo pipefail

# ── Arguments ────────────────────────────────────────────────────────────────────────────────

TEMPLATE=""
TARGET=""
DRY_RUN=false
BACKUP=false

while [[ $# -gt 0 ]]; do
    case "$1" in
        --template) TEMPLATE="$2"; shift 2 ;;
        --target)   TARGET="$2";   shift 2 ;;
        --dry-run)  DRY_RUN=true;  shift   ;;
        --backup)   BACKUP=true;   shift   ;;
        *) echo "Unknown option: $1" >&2; exit 1 ;;
    esac
done

[[ -z "$TEMPLATE" ]] && { echo "Error: --template required" >&2; exit 1; }
[[ -z "$TARGET"   ]] && { echo "Error: --target required"   >&2; exit 1; }
[[ -f "$TEMPLATE" ]] || { echo "Error: template not found: $TEMPLATE" >&2; exit 1; }
[[ -f "$TARGET"   ]] || { echo "Error: target not found: $TARGET"     >&2; exit 1; }

# ── Parse target → KEY → full definition block ───────────────────────────────────────────────

declare -A HOST_MAP   # KEY → complete definition line(s) from user's conf

_parse_target() {
    local in_block=false cur_key="" cur_block="" line

    while IFS= read -r line || [[ -n "$line" ]]; do
        if [[ "$in_block" == true ]]; then
            cur_block+="$line"$'\n'
            # Closing ) — optional trailing whitespace and comment
            if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
                HOST_MAP["$cur_key"]="$cur_block"
                in_block=false; cur_key=""; cur_block=""
            fi
        else
            # declare -A KEY=(
            if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
                cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
            # KEY=(
            elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
                cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
            # KEY=value  (simple scalar)
            elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
                HOST_MAP["${BASH_REMATCH[1]}"]="$line"$'\n'
            fi
        fi
    done < "$TARGET"
}

# ── Walk template — collect stats (must run in current shell so arrays persist) ──────────────

declare -a ADDED=() KEPT=() REMOVED=()
declare -A TMPL_SEEN=()

_collect_stats() {
    local in_block=false cur_key="" line

    while IFS= read -r line || [[ -n "$line" ]]; do
        if [[ "$in_block" == true ]]; then
            if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
                in_block=false
                TMPL_SEEN["$cur_key"]=1
                if [[ -n "${HOST_MAP[$cur_key]+_}" ]]; then KEPT+=("$cur_key")
                else                                         ADDED+=("$cur_key"); fi
                cur_key=""
            fi
        else
            if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
                cur_key="${BASH_REMATCH[1]}"; in_block=true
            elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
                cur_key="${BASH_REMATCH[1]}"; in_block=true
            elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
                local k="${BASH_REMATCH[1]}"
                TMPL_SEEN["$k"]=1
                if [[ -n "${HOST_MAP[$k]+_}" ]]; then KEPT+=("$k")
                else                                   ADDED+=("$k"); fi
            fi
        fi
    done < "$TEMPLATE"

    for key in "${!HOST_MAP[@]}"; do
        [[ -z "${TMPL_SEEN[$key]+_}" ]] && REMOVED+=("$key")
    done
}

# ── Walk template — write merged output ──────────────────────────────────────────────────────
# Runs in a subshell (stdout redirected) — array mutations are intentionally discarded here.

_write_merged() {
    local in_block=false cur_key="" cur_block="" line

    while IFS= read -r line || [[ -n "$line" ]]; do
        if [[ "$in_block" == true ]]; then
            cur_block+="$line"$'\n'
            if [[ "$line" =~ ^[[:space:]]*\)[[:space:]]*(#.*)?$ ]]; then
                in_block=false
                if [[ -n "${HOST_MAP[$cur_key]+_}" ]]; then printf '%s' "${HOST_MAP[$cur_key]}"
                else                                        printf '%s' "$cur_block"; fi
                cur_key=""; cur_block=""
            fi
        else
            if [[ "$line" =~ ^[[:space:]]*declare[[:space:]]+-[a-zA-Z]+[[:space:]]+([A-Z0-9_]+)[[:space:]]*=\( ]]; then
                cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
            elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*=\( ]]; then
                cur_key="${BASH_REMATCH[1]}"; in_block=true; cur_block="$line"$'\n'
            elif [[ "$line" =~ ^[[:space:]]*([A-Z0-9_]+)[[:space:]]*= ]]; then
                local k="${BASH_REMATCH[1]}"
                if [[ -n "${HOST_MAP[$k]+_}" ]]; then printf '%s' "${HOST_MAP[$k]}"
                else                                  printf '%s\n' "$line"; fi
            else
                printf '%s\n' "$line"
            fi
        fi
    done < "$TEMPLATE"
}

# ── Run ───────────────────────────────────────────────────────────────────────────────────────

_parse_target
_collect_stats

# ── Report ────────────────────────────────────────────────────────────────────────────────────

TARGET_NAME="$(basename "$TARGET")"
echo ""
echo "── conf_upgrade: $TARGET_NAME ──────────────────────────────────────────"

if [[ ${#ADDED[@]} -gt 0 ]]; then
    echo "  ADDED   (new — fill in your values where needed):"
    for k in "${ADDED[@]}"; do echo "    + $k"; done
fi

if [[ ${#REMOVED[@]} -gt 0 ]]; then
    echo "  REMOVED (deprecated — no longer in this version):"
    for k in "${REMOVED[@]}"; do echo "    - $k"; done
fi

echo "  KEPT    ${#KEPT[@]} existing vars — your values preserved"

if [[ ${#ADDED[@]} -eq 0 && ${#REMOVED[@]} -eq 0 ]]; then
    echo "  Already up to date — no changes needed."
    echo "────────────────────────────────────────────────────────────────────────"
    echo ""
    exit 0
fi

echo "────────────────────────────────────────────────────────────────────────"
echo ""

# ── Apply ─────────────────────────────────────────────────────────────────────────────────────

if [[ "$DRY_RUN" == true ]]; then
    echo "(dry-run — no changes written)"
    exit 0
fi

TMPOUT="$(mktemp)"
trap 'rm -f "$TMPOUT"' EXIT

_write_merged > "$TMPOUT"

if [[ "$BACKUP" == true ]]; then
    cp "$TARGET" "${TARGET}.bak"
    echo "Backup: ${TARGET}.bak"
fi

cp "$TMPOUT" "$TARGET"
echo "Updated: $TARGET"
