#!/bin/bash
set -euo pipefail

TMPDIR="/tmp/attain-api-install"
SITE_ROOT="/var/www"

# Defaults
FORCE=false
TARGET=""
FORCED_PORT=""
DBNAME=""
SELECT=false
NONX=false
WHATIF=false
declare -A TEMPLATE_PARAMS=()   # lower-cased placeholder name -> value (from --<Name> <value>)

usage() {
    cat <<'EOF'

install.sh - install the latest attain-api build into one <site>.spattain.com site.

USAGE
  install.sh -t <site> [-p <port>] [-d <dbname>] [-f] [--nonx] [-n] [--<TemplateVar> <value> ...]
  install.sh -s [--nonx] [-n]
  install.sh -h

OPTIONS
  -t, --target <site>   Site name (without .spattain.com).
  -p, --port <n>        Kestrel port. Default: keep the site's port; new site = highest other
                        site port + 1 (min 4090, skipping ports in use).
  -d, --dbname <name>   Database base name -> SP_<name>, SP_<name>_logs. Default: <site> for
                        new sites; existing sites keep their databases unless given.
  -f, --force           Download and extract the build again even if already extracted.
  -s, --select          Interactive: prompt for target/port/dbname (and, for a new site,
                        template values) when not given, show the plan, confirm.
      --nonx            Do not reload nginx (config is still written and tested).
  -n, --whatif          Show the plan; download/copy/write/restart nothing. (alias: --dry-run)
  -h, --help            Show this help.

NEW SITES: appsettings.Template.json
  A new site's appsettings.Production.json is made from appsettings.Template.json, taken from the
  current directory, else from http://<prefix>APP1/data/appsettings.Template.json.
  Placeholders look like <<Name:default>> or <<Name>> (no default = required). Each value comes from:
    --<Name> <value> or --<Name>=<value>   (e.g. --RaptorDbHost 10.0.1.6; name is case-insensitive)
    env SPAttain<Name>                    (e.g. SPAttainDbPassword=...)
    --select prompt                       (Enter = default)
    the default
  <<Target>> is the site name. Values are inserted as plain text (JSON-escaped by the script).
  Defaults may use $Name$ for: $Get-SourceHostPrefix$ (e.g. atp2), $Get-SourceHost$ (e.g. ATP2APP1),
  $Target$ (site name), $Get-UppercaseTarget$ (site name upper-cased, e.g. BOMBO),
  $DbName$ (--dbname, else site name).
  e.g. <<FtpUser:$Get-SourceHostPrefix$deploy>> -> atp2deploy
  Port and database names are then set as for any site (see --port / --dbname).

EXAMPLES
  install.sh -t dabew
  install.sh -t newsite -d newsite_db --whatif
  install.sh -t newsite --RaptorDbHost 10.0.1.6 --DbPassword '...' --DbReadPassword '...'
  install.sh --select

EOF
}

# ---- Parse parameters ----
while [[ $# -gt 0 ]]; do
    case "$1" in
        -f|--force)
            FORCE=true
            shift
            ;;
        -t|--target)
            TARGET="$2"
            shift 2
            ;;
        -p|--port)
            FORCED_PORT="$2"
            shift 2
            ;;
        -d|--dbname)
            DBNAME="$2"
            shift 2
            ;;
        -s|--select)
            SELECT=true
            shift
            ;;
        --nonx)
            NONX=true
            shift
            ;;
        -n|--whatif|--dry-run)
            WHATIF=true
            shift
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        --*=*)
            # Template placeholder value: --Name=value
            name="${1%%=*}"; name="${name#--}"
            if [[ ! "$name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
                echo "Unknown parameter: $1 (see --help)"
                exit 1
            fi
            TEMPLATE_PARAMS["${name,,}"]="${1#*=}"
            shift
            ;;
        --[A-Za-z_]*)
            # Template placeholder value: --Name value
            name="${1#--}"
            if [[ ! "$name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ || $# -lt 2 ]]; then
                echo "Unknown parameter or missing value: $1 (see --help)"
                exit 1
            fi
            TEMPLATE_PARAMS["${name,,}"]="$2"
            shift 2
            ;;
        *)
            echo "Unknown parameter: $1 (see --help)"
            exit 1
            ;;
    esac
done

# ---- Helpers ----
# Version regex: letter revision is OPTIONAL
# (e.g. attain-api-2026.05.27-mp.zip has none; attain-api-2026.07.08b-mp.zip has 'b').
ZIP_VERSION_RX='^attain-api-([0-9]{4}\.[0-9]{2}\.[0-9]{2})([A-Za-z]?)(-.*)?\.zip$'

get_source_host_prefix() {
    # This machine's AT[BP]<n> hostname prefix, lower-case (e.g. atp2); fallback atb2.
    local cn
    cn=$(hostname -s | tr '[:upper:]' '[:lower:]')
    if [[ "$cn" =~ ^(at[bp][0-9]+) ]]; then
        echo "${BASH_REMATCH[1]}"
    else
        echo "atb2"
    fi
}

get_source_host() {
    # <prefix>APP1, e.g. ATP2APP1; fallback ATB2APP1.
    echo "$(get_source_host_prefix | tr '[:lower:]' '[:upper:]')APP1"
}

get_latest_remote_zip() {
    local html name
    if ! html=$(curl -s --fail --connect-timeout 10 "$BASE_URL/") || [[ -z "$html" ]]; then
        echo "ERROR: Could not list $BASE_URL/. Check the host." >&2
        return 1
    fi

    local latest
    latest=$(
        grep -oE 'href="attain-api-[^"?]+\.zip"' <<<"$html" | sed -E 's/^href="//; s/"$//' | sort -u |
        while read -r name; do
            name=$(printf '%b' "${name//%/\\x}")   # URL-decode
            if [[ "$name" =~ $ZIP_VERSION_RX ]]; then
                printf '%s%s\t%s\n' "${BASH_REMATCH[1]}" "$(tr '[:upper:]' '[:lower:]' <<<"${BASH_REMATCH[2]}")" "$name"
            fi
        done | LC_ALL=C sort -t $'\t' -k1,1r | head -n 1 | cut -f2
    ) || true

    if [[ -z "$latest" ]]; then
        echo "ERROR: No attain-api-*.zip builds found at $BASE_URL/" >&2
        return 1
    fi
    echo "$latest"
}

# The port lives in appsettings.Production.json (Kestrel:Endpoints:Http:Url); ecosystem.config.js
# reads it from there for pm2, and nginx must proxy to the same port.
port_from_url() {
    # Same pattern ecosystem.config.js uses.
    sed -nE 's#^.*:([0-9]+)(/.*)?$#\1#p' <<<"$1"
}

site_port() {
    port_from_url "$(sudo jq -r '.Kestrel.Endpoints.Http.Url // empty' "$1")"
}

other_site_ports() {
    # Ports claimed by other sites: nginx proxy_pass targets + each site's Kestrel URL.
    local f d
    {
        for f in /etc/nginx/sites-available/*; do
            [[ -f "$f" && "$f" != "$NGINX_CONF" ]] || continue
            grep -oE 'proxy_pass[[:space:]]+http://localhost:[0-9]+' "$f" | grep -oE '[0-9]+$' || true
        done
        for d in "$SITE_ROOT"/*.spattain.com; do
            [[ -d "$d" && "$d" != "$TARGET_DIR" ]] || continue
            f="$d/appsettings.Production.json"
            sudo test -f "$f" || continue
            site_port "$f" 2>/dev/null || echo "WARNING: Could not read port from $f" >&2
        done
    } | sort -nu
}

port_listening() {
    ss -ltnH "sport = :$1" | grep -q .
}

# Builds the jq filter that sets Instance + port, and database names for new sites or --dbname
# (existing sites keep theirs).
appsettings_filter() {
    local filter='.Attain.Instance = $instance
  | .Kestrel.Endpoints.Http.Url |= (if . then sub(":[0-9]+"; ":\($port)") else "http://localhost:\($port)" end)'
    if [[ "$IS_NEW_SITE" == true || -n "$DBNAME" ]]; then
        filter='def setdb($k; $v): if .ConnectionStrings[$k] then .ConnectionStrings[$k] |= sub("Initial Catalog=[^;]+"; "Initial Catalog=\($v)") else . end;
  '"$filter"'
  | setdb("AttainDbContext"; "SP_\($db)")
  | setdb("AttainDbReadContext"; "SP_\($db)")
  | setdb("LogsDbContext"; "SP_\($db)_logs")'
    fi
    echo "$filter"
}

# Writes the edited appsettings to stdout.
render_appsettings() {
    sudo jq \
        --arg instance "$TARGET" \
        --arg port "$KESTREL_PORT" \
        --arg db "${DBNAME:-$TARGET}" \
        "$(appsettings_filter)" \
        "$1"
}

# Prints "what: old -> new" for each changed setting (catalog names only, never passwords).
describe_appsettings_changes() {
    local old="$1" new="$2" entry what query a b
    for entry in \
        'Attain:Instance|.Attain.Instance // ""' \
        'Kestrel url|.Kestrel.Endpoints.Http.Url // ""' \
        'ConnectionStrings:AttainDbContext|(.ConnectionStrings.AttainDbContext // "") | ([capture("Initial Catalog=(?<c>[^;]+)").c] | first // "")' \
        'ConnectionStrings:AttainDbReadContext|(.ConnectionStrings.AttainDbReadContext // "") | ([capture("Initial Catalog=(?<c>[^;]+)").c] | first // "")' \
        'ConnectionStrings:LogsDbContext|(.ConnectionStrings.LogsDbContext // "") | ([capture("Initial Catalog=(?<c>[^;]+)").c] | first // "")'
    do
        what="${entry%%|*}"
        query="${entry#*|}"
        a=$(sudo jq -r "$query" "$old")
        b=$(jq -r "$query" "$new")
        [[ "$a" != "$b" ]] && echo "$what: $a -> $b"
    done
    return 0
}

ask() {
    # ask <prompt> <hint> -> answer on stdout (reads from the terminal)
    local prompt="$1" hint="$2" answer
    [[ -n "$hint" ]] && prompt="$prompt [$hint]"
    read -r -p "$prompt: " answer < /dev/tty
    echo "${answer// /}"
}

# ---- appsettings.Template.json (new sites) ----
# Placeholders: <<Name:default>> or <<Name>> (required). <<Target>> is the site name.
PLACEHOLDER_RX='<<[A-Za-z_][A-Za-z0-9_]*(:[^>]*)?>>'
declare -a TEMPLATE_VARS=()        # placeholder names, in order of first appearance
declare -A TEMPLATE_DEFAULT=()     # lower-cased name -> default
declare -A TEMPLATE_HAS_DEFAULT=() # lower-cased name -> true
declare -A TEMPLATE_VALUE=()       # lower-cased name -> resolved value
declare -A TEMPLATE_SOURCE=()      # lower-cased name -> where the value came from (MISSING if none)

is_secret_name() { [[ "${1,,}" =~ password|token|secret|key ]]; }

# $Name$ inside a placeholder default, e.g. <<FtpUser:$Get-SourceHostPrefix$deploy>>.
# Only these names are allowed: the template may come over HTTP, so it must not run arbitrary code.
template_expression() {
    case "${1,,}" in
        get-sourcehostprefix) get_source_host_prefix ;;
        get-sourcehost)       get_source_host ;;
        target)               echo "$TARGET" ;;
        get-uppercasetarget)  echo "${TARGET^^}" ;;
        dbname)               echo "${DBNAME:-$TARGET}" ;;
        *)
            echo "ERROR: Unknown \$$1\$ in appsettings.Template.json. Available: \$DbName\$, \$Get-SourceHost\$, \$Get-SourceHostPrefix\$, \$Get-UppercaseTarget\$, \$Target\$" >&2
            return 1
            ;;
    esac
}

expand_template_expressions() {
    local text="$1" out="" value rx='\$([A-Za-z][A-Za-z0-9_-]*)\$'
    while [[ "$text" =~ $rx ]]; do
        value=$(template_expression "${BASH_REMATCH[1]}") || return 1
        out+="${text%%"${BASH_REMATCH[0]}"*}$value"
        text="${text#*"${BASH_REMATCH[0]}"}"
    done
    printf '%s' "$out$text"
}

load_template() {
    # Sets TEMPLATE_TEXT / TEMPLATE_FROM. Current directory first, then the build server.
    if [[ -f "./appsettings.Template.json" ]]; then
        TEMPLATE_FROM="$(pwd)/appsettings.Template.json"
        TEMPLATE_TEXT=$(cat "./appsettings.Template.json")
    else
        TEMPLATE_FROM="$BASE_URL/appsettings.Template.json"
        if ! TEMPLATE_TEXT=$(curl -s --fail --connect-timeout 10 "$TEMPLATE_FROM"); then
            echo "ERROR: No appsettings.Template.json in $(pwd) and could not fetch $TEMPLATE_FROM." >&2
            return 1
        fi
    fi
}

collect_template_vars() {
    # First default given for a name wins.
    local token name def lc
    while read -r token; do
        [[ -n "$token" ]] || continue
        token="${token#<<}"; token="${token%>>}"
        name="${token%%:*}"
        lc="${name,,}"
        if [[ -z "${TEMPLATE_SOURCE[$lc]+x}" ]]; then
            TEMPLATE_VARS+=("$name")
            TEMPLATE_SOURCE[$lc]=""
        fi
        if [[ "$token" == *:* && -z "${TEMPLATE_HAS_DEFAULT[$lc]+x}" ]]; then
            TEMPLATE_DEFAULT[$lc]=$(expand_template_expressions "${token#*:}") || exit 1
            TEMPLATE_HAS_DEFAULT[$lc]=true
        fi
    done < <(grep -oE "$PLACEHOLDER_RX" <<<"$TEMPLATE_TEXT" || true)
}

resolve_template_vars() {
    # Order: --Name param, env SPAttain<Name>, --select prompt, default.
    local name lc env_name hint value
    for name in "${TEMPLATE_VARS[@]}"; do
        lc="${name,,}"
        env_name="SPAttain$name"
        if [[ "$lc" == "target" ]]; then
            TEMPLATE_VALUE[$lc]="$TARGET"; TEMPLATE_SOURCE[$lc]="--target"
        elif [[ -n "${TEMPLATE_PARAMS[$lc]+x}" ]]; then
            TEMPLATE_VALUE[$lc]="${TEMPLATE_PARAMS[$lc]}"; TEMPLATE_SOURCE[$lc]="--$name"
        elif [[ -n "${!env_name:-}" ]]; then
            TEMPLATE_VALUE[$lc]="${!env_name}"; TEMPLATE_SOURCE[$lc]="env $env_name"
        elif [[ "$SELECT" == true ]]; then
            if [[ -n "${TEMPLATE_HAS_DEFAULT[$lc]+x}" ]]; then hint="Enter = ${TEMPLATE_DEFAULT[$lc]}"; else hint="required"; fi
            while true; do
                if is_secret_name "$name"; then
                    read -r -s -p "$name [$hint]: " value < /dev/tty; echo > /dev/tty
                else
                    read -r -p "$name [$hint]: " value < /dev/tty
                fi
                if [[ -n "$value" ]]; then
                    TEMPLATE_VALUE[$lc]="$value"; TEMPLATE_SOURCE[$lc]="entered"; break
                elif [[ -n "${TEMPLATE_HAS_DEFAULT[$lc]+x}" ]]; then
                    TEMPLATE_VALUE[$lc]="${TEMPLATE_DEFAULT[$lc]}"; TEMPLATE_SOURCE[$lc]="default"; break
                fi
            done
        elif [[ -n "${TEMPLATE_HAS_DEFAULT[$lc]+x}" ]]; then
            TEMPLATE_VALUE[$lc]="${TEMPLATE_DEFAULT[$lc]}"; TEMPLATE_SOURCE[$lc]="default"
        else
            TEMPLATE_SOURCE[$lc]="MISSING"
        fi
    done
}

render_template() {
    # Values are plain text: JSON-escape them for the string they land in.
    local text="$TEMPLATE_TEXT" token name value bs='\' q='"'
    while read -r token; do
        [[ -n "$token" ]] || continue
        name="${token#<<}"; name="${name%>>}"; name="${name%%:*}"
        value="${TEMPLATE_VALUE[${name,,}]}"
        value="${value//"$bs"/"$bs$bs"}"
        value="${value//"$q"/"$bs$q"}"
        text="${text//"$token"/"$value"}"
    done < <(grep -oE "$PLACEHOLDER_RX" <<<"$TEMPLATE_TEXT" | sort -u)
    printf '%s\n' "$text"
}

# ---- Interactive prompts (--select) ----
if [[ "$SELECT" == true ]]; then
    if [[ ! -t 0 && ! -r /dev/tty ]]; then
        echo "ERROR: --select needs an interactive terminal. Pass --target etc. instead."
        exit 1
    fi
    if [[ -z "$TARGET" ]]; then
        existing=$(find "$SITE_ROOT" -maxdepth 1 -type d -name '*.spattain.com' -printf '%f\n' 2>/dev/null \
                   | sed 's/\.spattain\.com$//' | sort | paste -sd, - | sed 's/,/, /g')
        [[ -n "$existing" ]] && echo "Existing sites: $existing"
        while [[ -z "$TARGET" ]]; do TARGET=$(ask "Target site" ""); done
    fi
    sel_settings="$SITE_ROOT/${TARGET}.spattain.com/appsettings.Production.json"
    if [[ -z "$FORCED_PORT" ]]; then
        hint="Enter = auto"
        if sudo test -f "$sel_settings"; then hint="Enter = keep $(site_port "$sel_settings" 2>/dev/null || true)"; fi
        FORCED_PORT=$(ask "Kestrel port" "$hint")
    fi
    if [[ -z "$DBNAME" ]]; then
        if sudo test -f "$sel_settings"; then hint="Enter = keep current databases"; else hint="Enter = $TARGET"; fi
        DBNAME=$(ask "Database base name" "$hint")
    fi
fi

# ---- Validate ----
if [[ -z "$TARGET" ]]; then
    echo "ERROR: --target <sitename> is required (or use --select)."
    exit 1
fi
if [[ ! "$TARGET" =~ ^[A-Za-z0-9][A-Za-z0-9-]*$ ]]; then
    echo "ERROR: --target must be a plain site name like 'dabew' (got '$TARGET')."
    exit 1
fi
if [[ -n "$FORCED_PORT" && ! "$FORCED_PORT" =~ ^[0-9]+$ ]]; then
    echo "ERROR: --port must be a number (got '$FORCED_PORT')."
    exit 1
fi

echo "=== Starting installation for $TARGET ==="

TARGET_DIR="$SITE_ROOT/${TARGET}.spattain.com"
APPSETTINGS_FILE="$TARGET_DIR/appsettings.Production.json"
NGINX_CONF="/etc/nginx/sites-available/${TARGET}.spattain.com"
NGINX_LINK="/etc/nginx/sites-enabled/${TARGET}.spattain.com"
IS_NEW_SITE=true
if sudo test -f "$APPSETTINGS_FILE"; then IS_NEW_SITE=false; fi
BASE_URL="http://$(get_source_host)/data"

# ---- New site: fill in appsettings.Template.json ----
TEMPLATE_FROM=""
TEMPLATE_TEXT=""
RENDERED_TEMPLATE=""
MISSING_TEMPLATE_VARS=""
if [[ "$IS_NEW_SITE" == true ]]; then
    load_template
    collect_template_vars
    for lc in "${!TEMPLATE_PARAMS[@]}"; do
        if [[ -z "${TEMPLATE_SOURCE[$lc]+x}" ]]; then
            echo "ERROR: Unknown parameter --$lc. Template placeholders: ${TEMPLATE_VARS[*]}"
            exit 1
        fi
    done
    resolve_template_vars
    for name in "${TEMPLATE_VARS[@]}"; do
        if [[ "${TEMPLATE_SOURCE[${name,,}]}" == "MISSING" ]]; then
            MISSING_TEMPLATE_VARS="${MISSING_TEMPLATE_VARS:+$MISSING_TEMPLATE_VARS, }$name"
        fi
    done
    if [[ -z "$MISSING_TEMPLATE_VARS" ]]; then
        RENDERED_TEMPLATE=$(render_template)
        if ! jq empty <<<"$RENDERED_TEMPLATE" 2>/dev/null; then
            echo "ERROR: appsettings.Template.json ($TEMPLATE_FROM) is not valid JSON once filled in."
            exit 1
        fi
    fi
elif [[ ${#TEMPLATE_PARAMS[@]} -gt 0 ]]; then
    echo "WARNING: Ignored for an existing site (the template is only used for new sites): $(printf -- '--%s ' "${!TEMPLATE_PARAMS[@]}")"
fi

# ---- 0. Find latest build on the server ----
LATEST=$(get_latest_remote_zip)
ZIP_URL="$BASE_URL/$LATEST"
VERSION_FILE="$TMPDIR/.version"
EXTRACT_DIR="$TMPDIR/extracted"
CURRENT_VERSION=""
if [[ -f "$VERSION_FILE" ]]; then
    CURRENT_VERSION=$(tr -d '[:space:]' < "$VERSION_FILE")
fi
NEED_DOWNLOAD=true
if [[ "$CURRENT_VERSION" == "$LATEST" && -d "$EXTRACT_DIR" && "$FORCE" == false ]]; then
    NEED_DOWNLOAD=false
fi

# ---- 1. Plan: Kestrel port ----
SITE_PORT=""
if [[ "$IS_NEW_SITE" == false ]]; then SITE_PORT=$(site_port "$APPSETTINGS_FILE"); fi
USED_PORTS=$(other_site_ports)
is_used() { grep -qx "$1" <<<"$USED_PORTS"; }

if [[ -n "$FORCED_PORT" ]]; then
    KESTREL_PORT=$FORCED_PORT
    if is_used "$KESTREL_PORT"; then
        echo "ERROR: Port $KESTREL_PORT is already used by another site."
        exit 1
    fi
    if [[ "$KESTREL_PORT" != "$SITE_PORT" ]] && port_listening "$KESTREL_PORT"; then
        echo "WARNING: Port $KESTREL_PORT is already listening (not by this site's current port)."
    fi
    PORT_REASON="requested"
elif [[ "$IS_NEW_SITE" == false && -n "$SITE_PORT" ]] && ! is_used "$SITE_PORT"; then
    KESTREL_PORT=$SITE_PORT
    PORT_REASON="existing site port"
else
    if [[ "$IS_NEW_SITE" == false && -n "$SITE_PORT" ]]; then
        echo "WARNING: Site port $SITE_PORT clashes with another site; allocating a new one."
    fi
    HIGHEST=$(tail -n 1 <<<"$USED_PORTS")
    KESTREL_PORT=4090
    if [[ -n "$HIGHEST" && $((HIGHEST + 1)) -gt $KESTREL_PORT ]]; then
        KESTREL_PORT=$((HIGHEST + 1))
    fi
    # Skip ports already held by something that isn't a site.
    while port_listening "$KESTREL_PORT"; do
        KESTREL_PORT=$((KESTREL_PORT + 1))
    done
    PORT_REASON="allocated"
fi

# ---- 1. Plan: appsettings, hosts, nginx ----
PLANNED_JSON_CHANGES=""
if [[ "$IS_NEW_SITE" == false ]]; then
    PLAN_JSON=$(mktemp)
    render_appsettings "$APPSETTINGS_FILE" > "$PLAN_JSON"
    PLANNED_JSON_CHANGES=$(describe_appsettings_changes "$APPSETTINGS_FILE" "$PLAN_JSON")
    rm -f "$PLAN_JSON"
fi

HOSTS_LINE="127.0.0.1 ${TARGET}.spattain.com"
NEED_HOSTS=true
if grep -qE "127\.0\.0\.1\s+${TARGET}\.spattain\.com" /etc/hosts; then NEED_HOSTS=false; fi

if [[ ! -f "$NGINX_CONF" ]]; then
    NGINX_ACTION="create"
elif grep -qE "proxy_pass[[:space:]]+http://localhost:${KESTREL_PORT};" "$NGINX_CONF"; then
    NGINX_ACTION="unchanged"
else
    NGINX_ACTION="update port"
fi

echo ""
echo "Plan"
echo "  Site        : $TARGET_DIR ($([[ "$IS_NEW_SITE" == true ]] && echo 'new site' || echo 'existing site'))"
echo "  Build       : $LATEST  ($([[ "$NEED_DOWNLOAD" == true ]] && echo 'download + extract' || echo 'already extracted, reused'))"
echo "  Port        : $KESTREL_PORT ($PORT_REASON)"
if [[ "$IS_NEW_SITE" == true ]]; then
    echo "  appsettings : from template $TEMPLATE_FROM; Instance=$TARGET, port=$KESTREL_PORT, DB=SP_${DBNAME:-$TARGET}"
    width=0
    for name in "${TEMPLATE_VARS[@]}"; do
        if (( ${#name} > width )); then width=${#name}; fi
    done
    for name in "${TEMPLATE_VARS[@]}"; do
        lc="${name,,}"
        [[ "$lc" == "target" ]] && continue
        if [[ "${TEMPLATE_SOURCE[$lc]}" == "MISSING" ]]; then
            shown="MISSING (required)"
        elif is_secret_name "$name"; then
            shown="********  (${TEMPLATE_SOURCE[$lc]})"
        else
            shown="${TEMPLATE_VALUE[$lc]}  (${TEMPLATE_SOURCE[$lc]})"
        fi
        printf '                %-*s = %s\n' "$width" "$name" "$shown"
    done
elif [[ -n "$PLANNED_JSON_CHANGES" ]]; then
    echo "  appsettings : kept, with changes:"
    sed 's/^/                /' <<<"$PLANNED_JSON_CHANGES"
else
    echo "  appsettings : kept, no changes"
fi
echo "  hosts       : $([[ "$NEED_HOSTS" == true ]] && echo "add '$HOSTS_LINE'" || echo 'entry exists')"
echo "  nginx conf  : $NGINX_ACTION"
echo "  nginx       : $([[ "$NONX" == true ]] && echo 'test only, no reload (--nonx)' || echo 'test + reload')"
echo "  pm2         : restart $TARGET"
echo ""

if [[ -n "$MISSING_TEMPLATE_VARS" ]]; then
    msg="Missing template values: $MISSING_TEMPLATE_VARS. Pass --<Name> <value>, set SPAttain<Name>, or use --select."
    if [[ "$WHATIF" == true ]]; then echo "WARNING: $msg"; else echo "ERROR: $msg"; exit 1; fi
fi
if [[ "$WHATIF" == true ]]; then
    echo "WhatIf: nothing was changed."
    exit 0
fi
if [[ "$SELECT" == true ]]; then
    answer=$(ask "Proceed? (y/N)" "")
    if [[ ! "$answer" =~ ^([yY]|[yY][eE][sS])$ ]]; then
        echo "Cancelled. Nothing was changed."
        exit 0
    fi
fi

# ---- 2. Download + extract (shared by all sites; reused when this version is already extracted) ----
mkdir -p "$TMPDIR"

if [[ "$NEED_DOWNLOAD" == false ]]; then
    echo "Package $LATEST already downloaded; reusing it. Use --force to download again."
else
    echo "Downloading: $ZIP_URL ..."
    curl -fSL "$ZIP_URL" -o "$TMPDIR/package.zip"

    echo "Extracting package..."
    rm -rf "$EXTRACT_DIR"
    mkdir -p "$EXTRACT_DIR"
    unzip -qo "$TMPDIR/package.zip" -d "$EXTRACT_DIR"
    echo "$LATEST" > "$VERSION_FILE"
fi

# ---- Stop PM2 process if running ----
echo "Stopping PM2 process for $TARGET (if exists)..."
pm2 delete "$TARGET" || true

# ---- 3. Deploy to /var/www/<sitename>.spattain.com (the site's own appsettings.Production.json is kept) ----
echo "Deploying to: $TARGET_DIR"

SAVED_APPSETTINGS="$TMPDIR/${TARGET}.appsettings.Production.json.bak"
if [[ "$IS_NEW_SITE" == false ]]; then
    sudo cp -p "$APPSETTINGS_FILE" "$SAVED_APPSETTINGS"
fi

if [[ -d "$TARGET_DIR" ]]; then
    echo "Cleaning old deployment..."
    sudo rm -rf "${TARGET_DIR:?}/"*
else
    sudo mkdir -p "$TARGET_DIR"
fi

sudo cp -r "$EXTRACT_DIR/"* "$TARGET_DIR/"

if [[ "$IS_NEW_SITE" == false ]]; then
    sudo cp -p "$SAVED_APPSETTINGS" "$APPSETTINGS_FILE"
    sudo rm -f "$SAVED_APPSETTINGS"
fi
echo "Deployment complete."

if [[ "$IS_NEW_SITE" == true ]]; then
    # The package ships no appsettings.Production.json (it is used for updates); new sites get the template.
    printf '%s\n' "$RENDERED_TEMPLATE" | sudo tee "$APPSETTINGS_FILE" > /dev/null
    echo "appsettings.Production.json created from $TEMPLATE_FROM."
fi

# ---- 4. Update appsettings.Production.json ----
TMP_JSON=$(mktemp)
render_appsettings "$APPSETTINGS_FILE" > "$TMP_JSON"

if [[ "$(sudo jq -cS . "$APPSETTINGS_FILE")" != "$(jq -cS . "$TMP_JSON")" ]]; then
    # tee keeps the existing file's owner and permissions.
    sudo tee "$APPSETTINGS_FILE" < "$TMP_JSON" > /dev/null
    echo "appsettings.Production.json updated."
else
    echo "appsettings.Production.json already up to date."
fi
rm -f "$TMP_JSON"

# ---- 5. Ensure /etc/hosts contains local entry ----
if [[ "$NEED_HOSTS" == true ]]; then
    echo "Adding hosts entry..."
    echo "$HOSTS_LINE" | sudo tee -a /etc/hosts > /dev/null
    echo "/etc/hosts updated."
else
    echo "/etc/hosts already contains entry."
fi

# ---- 5. Create / update NGINX config ----
if [[ "$NGINX_ACTION" == "create" ]]; then
    echo "Creating nginx config: $NGINX_CONF"
    sudo tee "$NGINX_CONF" > /dev/null <<EOF
server {
    listen 4448;
    server_name ${TARGET}.spattain.com;

    location / {
        proxy_pass http://localhost:${KESTREL_PORT};
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_read_timeout 300s;
    }
}
EOF
elif [[ "$NGINX_ACTION" == "update port" ]]; then
    sudo sed -i -E "s#(proxy_pass[[:space:]]+http://localhost:)[0-9]+#\1${KESTREL_PORT}#" "$NGINX_CONF"
    echo "NGINX config updated to port ${KESTREL_PORT}: $NGINX_CONF"
else
    echo "NGINX config already exists: $NGINX_CONF"
fi

# ---- Enable site ----
if [[ ! -L "$NGINX_LINK" ]]; then
    echo "Enabling nginx site..."
    sudo ln -s "$NGINX_CONF" "$NGINX_LINK"
else
    echo "Nginx site already enabled."
fi

# ---- Fix ecosystem.config.js script entry (convert exe to dll) ----
ECOFILE="$TARGET_DIR/ecosystem.config.js"

if [[ -f "$ECOFILE" ]]; then
    echo "Updating ecosystem.config.js to use dotnet..."
    sudo sed -i 's/script:.*Attain\.Api\.exe"/script: "dotnet Attain.Api.dll"/' "$ECOFILE"
else
    echo "WARNING: No ecosystem.config.js found."
fi

# ---- 5. Start PM2 process ----
echo "Starting PM2 for $TARGET..."
cd "$TARGET_DIR"
pm2 start ecosystem.config.js --only "$TARGET"
pm2 save
echo "PM2 start completed."

# ---- 5. Test + reload nginx ----
echo "Testing nginx config..."
sudo nginx -t
if [[ "$NONX" == true ]]; then
    echo "Skipping nginx reload (--nonx)."
else
    echo "Reloading nginx..."
    sudo systemctl reload nginx
fi

echo "=== Installation for $TARGET completed successfully ==="
echo "Access your site at http://$TARGET.spattain.com (reverse proxy port: 4448 -> localhost:$KESTREL_PORT)"
if [[ "$NONX" == true && "$NGINX_ACTION" != "unchanged" ]]; then
    echo "WARNING: nginx was not reloaded (--nonx); the $NGINX_ACTION to $NGINX_CONF takes effect on the next nginx reload."
fi
