#!/bin/bash
# ==============================================================================================
# ============================= share_setup.sh =================================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Creates missing Unraid shares on the mirror during partnership onboarding.
# Safe to re-run — existing shares are never modified, only missing ones created.
#
# For each path in the owner's sync share lists (daily, weekly, critical,
# intermediate):
#   - Extracts the top-level Unraid share name
#   - Skips if the remote already has a .cfg for it
#   - Uses the local share .cfg as a template: substitutes the remote's detected
#     appdata pool, clears disk include/exclude (disk layouts differ per server)
#   - Writes the .cfg to remote /boot/config/shares/ and mkdir -p the directory
#   - For sub-paths (e.g. appdata-Fallback/Critical-Data), ensures the subdir
#     exists after the top-level share is in place
#
# Pool detection: reads the remote's appdata.cfg to find its cache pool name,
# so appdata-type shares land on the right pool without hardcoding.
#
# Media shares (shareUseCache=no in local .cfg) are created as array-only.
# Remote admin assigns disk sets from the Unraid UI after onboarding.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
#   1. Resolve the mirror and its Tailscale IP — unresolvable aborts before any remote call
#   2. Detect the remote's appdata cache pool from its own appdata.cfg (default: cache)
#   3. For each path across the owner's daily/weekly/critical/intermediate share lists:
#        a. Extract the top-level Unraid share name
#        b. Remote already has a .cfg for it? → skip, never modify
#        c. Otherwise use the LOCAL .cfg as a template:
#             - substitute the remote's detected pool
#             - clear disk include/exclude (disk layouts differ per server)
#        d. Write the .cfg to remote /boot/config/shares/ and mkdir -p the share directory
#        e. Sub-paths (e.g. appdata-Fallback/Critical-Data) get their subdir created after
#           the top-level share exists
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Create Only, Never Modify
#   An existing remote .cfg is always left alone. The remote admin may have deliberately
#   tuned a share's pool, allocation or disk set — this script has no way to tell an
#   intentional setting from a stale one, so it never overwrites.
#
# Disk Layout Is Not Portable
#   Include/exclude lists are cleared rather than copied, because the two servers have
#   different disks. Copying the owner's disk set onto a mirror with a different array
#   would produce a share pointing at disks that do not exist.
#
# Detect the Pool, Do Not Assume It
#   The remote's cache pool name is read from its own appdata.cfg rather than hardcoded or
#   copied from local. Pool names differ per server and a wrong one silently lands appdata
#   on the array.
#
# Media Shares Land Array-Only
#   Shares with shareUseCache=no are created without a pool assignment. Disk sets are the
#   remote admin's call from the Unraid UI after onboarding.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Enforcement
#   Reads $SSH_KEY from /root/.ssh and the local /boot/config/shares/*.cfg, and writes share
#   configs onto the mirror as root.
#
# Lock Acquisition
#   acquire_lock prevents concurrent runs. Two instances could both observe a share as
#   missing and race to create it.
#
# Host Detection
#   detect_hosts() resolves REMOTE_SERVER_NAME — the mirror this script targets.
#
# Mirror Resolution Guard
#   Aborts if REMOTE_SERVER_NAME is unset or its Tailscale IP cannot be resolved, before any
#   remote command is attempted.
#
# Existing Share Protection
#   A remote .cfg that already exists is never touched — see Create Only above.
#
# SSH Timeouts and BatchMode
#   Every remote call uses ConnectTimeout and BatchMode=yes, so an unreachable or
#   password-prompting mirror fails fast instead of hanging the onboarding run.
#
# Pool Fallback
#   An undetectable remote pool defaults to "cache" rather than writing an empty pool name
#   into the share config.
#
# Dry Run Support
#   --dry-run reports every share it would create and writes nothing.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf (aliased by detect_hosts())
#
#   HOST*_DAILY_SYNC_SHARES / _WEEKLY_ / _CRITICAL_ / _INTERMEDIATE_
#       The share lists this script reads. Any path appearing in one of these on the owner
#       is a share the mirror is expected to have.
#
#   SSH_KEY
#       Key used for every remote call. Written by ssh_setup.sh.
#
# master.conf
#
#   HOST* — hostnames, used to resolve the mirror via detect_hosts()
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# share_setup.sh
#     Create missing shares on the configured mirror. Skips existing.
#
# share_setup.sh --dry-run
#     Show what would be created without writing anything.
#
# ==============================================================================================

set -uo pipefail

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

# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
# Reads $SSH_KEY from /root/.ssh, reads local /boot/config/shares/*.cfg, and writes share
# configs onto the mirror as root.
[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }

# Writes share .cfg files to the mirror. Two concurrent runs could both see a share as
# missing and race to create it.
acquire_lock

detect_hosts

# ── Resolve mirror ────────────────────────────────────────────────────────────────────────────

[[ -z "${REMOTE_SERVER_NAME:-}" ]] && { error "Cannot determine mirror hostname — check HOST* in master.conf"; exit 1; }
MIRROR="$REMOTE_SERVER_NAME"

MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
[[ -z "$MIRROR_IP" ]] && { error "Cannot resolve $MIRROR Tailscale IP — is Tailscale running?"; exit 1; }

_ssh() { ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=no root@"$MIRROR_IP" "$@"; }
_scp() { scp -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=no "$@"; }

# ── Detect remote appdata pool ────────────────────────────────────────────────────────────────

REMOTE_POOL=$(_ssh "grep -oP '(?<=shareCachePool=\")[^\"]+' /boot/config/shares/appdata.cfg 2>/dev/null | head -1" 2>/dev/null || true)
REMOTE_POOL="${REMOTE_POOL:-cache}"
log "Remote appdata pool: $REMOTE_POOL"

# ── Collect paths from all sync share arrays ──────────────────────────────────────────────────

declare -a ALL_PATHS=()

_add_array() {
    local varname="$1"
    local -n _ref="$varname" 2>/dev/null || return 0
    for entry in "${_ref[@]+"${_ref[@]}"}"; do
        [[ -n "$entry" ]] && ALL_PATHS+=("${entry%%|*}")
    done
}

_add_array "${MY_ID}_DAILY_SYNC_SHARES"
_add_array "${MY_ID}_WEEKLY_SYNC_SHARES"
_add_array "${MY_ID}_CRITICAL_SYNC_SHARES"
_add_array "${MY_ID}_INTERMEDIATE_SYNC_SHARES"

# ── Separate top-level shares from sub-paths ──────────────────────────────────────────────────

declare -A SEEN_TOP=()
declare -A SEEN_SUB=()
declare -a TOP_SHARES=()
declare -a SUB_PATHS=()

for path in "${ALL_PATHS[@]+"${ALL_PATHS[@]}"}"; do
    [[ "$path" != /mnt/user/* ]] && continue
    rel="${path#/mnt/user/}"
    top="${rel%%/*}"
    [[ -z "${SEEN_TOP[$top]+_}" ]] && { TOP_SHARES+=("$top"); SEEN_TOP["$top"]=1; }
    if [[ "$rel" == */* && -z "${SEEN_SUB[$path]+_}" ]]; then
        SUB_PATHS+=("$path"); SEEN_SUB["$path"]=1
    fi
done

[[ ${#TOP_SHARES[@]} -eq 0 ]] && { echo "No shares to process."; exit 0; }

# ── Build .cfg for a share ────────────────────────────────────────────────────────────────────
# Uses local .cfg as template: substitutes pool, clears disk sets, drops comment.
# Falls back to a minimal cfg inferred from share name when no local .cfg exists.

_make_cfg() {
    local share_name="$1"
    local local_cfg="/boot/config/shares/${share_name}.cfg"

    if [[ -f "$local_cfg" ]]; then
        sed \
            -e "s|^shareCachePool=\"[^\"]*\"|shareCachePool=\"${REMOTE_POOL}\"|" \
            -e 's|^shareInclude="[^"]*"|shareInclude=""|' \
            -e 's|^shareExclude="[^"]*"|shareExclude=""|' \
            -e '/^shareComment=/d' \
            "$local_cfg"
    else
        local use_cache="no" pool="" allocator="mostfree"
        case "$share_name" in appdata*|Media_Server*)
            use_cache="yes"; pool="$REMOTE_POOL"; allocator="highwater" ;;
        esac
        printf 'shareUseCache="%s"\nshareCachePool="%s"\nshareCachePool2=""\n' "$use_cache" "$pool"
        printf 'shareCOW="auto"\nshareAllocator="%s"\nshareInclude=""\nshareExclude=""\n' "$allocator"
        printf 'shareFloor="0"\nshareExport="e"\nshareCaseSensitive="auto"\n'
        printf 'shareSecurity="private"\nshareReadList=""\nshareWriteList=""\n'
        printf 'shareVolsizelimit=""\nshareExportNFS="-"\nshareExportNFSFsid="0"\n'
        printf 'shareSecurityNFS="public"\nshareHostListNFS=""\n'
    fi
}

# ── Process each top-level share ──────────────────────────────────────────────────────────────

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

CREATED=0 SKIPPED=0

for share in "${TOP_SHARES[@]}"; do
    existing=$(_ssh "[[ -f '/boot/config/shares/${share}.cfg' ]] && echo yes || echo no" 2>/dev/null)

    if [[ "$existing" == "yes" ]]; then
        log "  exists: $share"
        (( SKIPPED++ )) || true
        continue
    fi

    if [[ "$DRY_RUN" == true ]]; then
        local_cfg="/boot/config/shares/${share}.cfg"
        if [[ -f "$local_cfg" ]]; then
            use_cache=$(grep -oP '(?<=shareUseCache=")[^"]+' "$local_cfg" || echo "?")
            echo "  [dry-run] CREATE $share  (useCache=$use_cache → pool: $REMOTE_POOL)"
        else
            echo "  [dry-run] CREATE $share  (no local cfg — minimal fallback)"
        fi
        (( CREATED++ )) || true
        continue
    fi

    _make_cfg "$share" > "$TMPFILE"
    _scp "$TMPFILE" "root@${MIRROR_IP}:/boot/config/shares/${share}.cfg" >/dev/null
    _ssh "mkdir -p /mnt/user/${share}" >/dev/null
    echo "  Created: $share ✅"
    (( CREATED++ )) || true
done

# ── Ensure sub-path directories exist ────────────────────────────────────────────────────────

for path in "${SUB_PATHS[@]+"${SUB_PATHS[@]}"}"; do
    if [[ "$DRY_RUN" == true ]]; then
        echo "  [dry-run] MKDIR $path"
        continue
    fi
    _ssh "mkdir -p '$path'" 2>/dev/null && log "  mkdir: $path" || true
done

# ── Summary ───────────────────────────────────────────────────────────────────────────────────

echo ""
echo "  Shares: $CREATED created, $SKIPPED already existed on $MIRROR"
[[ "$DRY_RUN" == true ]] && echo "  (dry-run — nothing written)"

exit 0
