#!/bin/bash
# ==============================================================================================
# ================================= Git Pull & Execute =========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Pulls the latest scripts from the Gitea repository via SSH. Lives at the repo
# root — sources load_config.sh from the same directory.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
#   1. detect_hosts() — identifies which server is running the pull (MY_ID)
#   2. Configure sparse checkout — exclude other servers' credential files
#   3. Pull or clone latest scripts from Gitea
#   4. Set executable permissions on all .sh files
#
# SPARSE CHECKOUT
#   Each server only receives its own host conf — never peer credentials:
#     HOST1 pulls:  master.conf + host1.conf + all scripts
#     HOST1 skips:  host2.conf, host3.conf etc.
#   Adding a new server: create host3.conf in the repo — all existing servers
#   automatically exclude it on next pull; new server gets only its own conf.
#
# GITEA LOCATION DETECTION
#   Detected at runtime — works through fallback:
#     Gitea local  → connects via local IP
#     Gitea remote → connects via Tailscale IP
#     Both fail    → falls back to GITEA_DOMAIN if configured
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Credentials Stay Separated
#   Sparse checkout is configured per-server on every pull, not just at clone
#   time. Ensures newly added host confs are automatically excluded on all
#   existing servers without any manual intervention.
#
# Runtime Location Detection
#   Gitea's IP is never hardcoded — the script probes whether Gitea is local
#   or remote on every run. Handles Gitea container restarts, migrations, and
#   Tailscale address changes automatically.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
#   Root check      — git operations on /boot require root
#   acquire_lock    — prevents concurrent pulls
#   detect_hosts()  — MY_ID required to build correct sparse checkout rules
#   Docker check    — Gitea container status probed before any SSH attempt
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
#   GITEA_CONTAINER  — Docker container name for Gitea
#   GITEA_REPO_PATH  — repo path on Gitea (e.g. Varaverk/varaverk.git)
#   GITEA_DOMAIN     — public domain fallback (optional)
#   TARGET_DIR       — local path to clone/pull into
#   GITEA_SSH_KEY    — SSH key for Gitea authentication
#   SSH_PORT         — Gitea SSH port (often 221 or 222)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# git_pull_execute.sh
#     Normal pull.
#
# git_pull_execute.sh --dry-run
#     Preview without making changes.
#
# git_pull_execute.sh --log
#     Verbose output.
#
# git_pull_execute.sh --status
#     Show config and exit.
#
# ==============================================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Root level script — load_config.sh is in the same directory
source "$SCRIPT_DIR/load_config.sh"

parse_args "$@"

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

if ! command -v docker &>/dev/null; then
    error "Docker command not found"
    exit 1
fi

# detect_hosts() sets MY_ID — needed for sparse checkout configuration
detect_hosts

# ==============================================================================================
# ━━━ Locate Gitea ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_CONTAINERS Locate Gitea ━━━"

if docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${GITEA_CONTAINER}$"; then
    # Gitea is running on this server — use local IP
    GITEA_IP=$(hostname -I | awk '{print $1}')
    log "Gitea running locally — connecting via $GITEA_IP"
else
    # Gitea not running locally — find it on the remote server via Tailscale
    log "Gitea not running locally — checking remote server"
    GITEA_IP=$(resolve_tailscale_ip "${REMOTE_SERVER_NAME}")
    if [[ -n "$GITEA_IP" ]]; then
        echo "  Gitea on $REMOTE_SERVER_NAME — connecting via Tailscale $GITEA_IP"
    elif [[ -n "${GITEA_DOMAIN:-}" ]]; then
        warn "Tailscale resolution failed — falling back to $GITEA_DOMAIN"
        GITEA_IP="$GITEA_DOMAIN"
    else
        error "Cannot find Gitea — local: not running, Tailscale: failed, domain: not configured"
        notify "Git pull failed on $(hostname) — cannot locate Gitea container" "Git Sync" "alert"
        exit 1
    fi
fi

REPO_SSH="git@${GITEA_IP}:${GITEA_REPO_PATH}"

require_var REPO_SSH
require_var TARGET_DIR
require_var GITEA_SSH_KEY
require_var SSH_PORT

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
    echo "$ICON_NET  Repo:        $REPO_SSH"
    echo "$ICON_GEAR Target:      $TARGET_DIR"
    echo "$ICON_GEAR SSH Key:     $GITEA_SSH_KEY"
    echo "$ICON_GEAR SSH Port:    $SSH_PORT"
    echo "$ICON_HOST My ID:       $MY_ID ($LOCAL_SERVER_NAME)"
    echo "$ICON_HOST Remote ID:   $REMOTE_ID ($REMOTE_SERVER_NAME)"
    echo "$ICON_NOTIFY Notify:    unRAID=${NOTIFY_UNRAID:-false} Discord=$([[ -n "${MY_DISCORD_WEBHOOK:-}" ]] && echo enabled || echo disabled)"
    echo "$ICON_GEAR Dry Run:     $DRY_RUN"
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

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

# ==============================================================================================
# ━━━ Sparse Checkout Configuration ━━━
# ==============================================================================================
# Build the list of host*.conf files that belong to OTHER servers.
# This server pulls everything EXCEPT those files.
# MY_ID is set by detect_hosts() — e.g. "HOST1"

configure_sparse_checkout() {
    local repo_dir="$1"

    log "Configuring sparse checkout for $MY_ID..."

    # Enable sparse checkout
    git -C "$repo_dir" config core.sparseCheckout true 2>/dev/null

    # Build exclusion list — all host*.conf files except MY_ID's
    local sparse_file="$repo_dir/.git/info/sparse-checkout"
    mkdir -p "$(dirname "$sparse_file")"

    # Start with: pull everything
    echo "/*" > "$sparse_file"

    # Exclude each other server's conf file
    # Find all host*.conf files present in the repo
    local excluded=0
    for conf_file in "$repo_dir"/host*.conf; do
        [[ -f "$conf_file" ]] || continue
        local conf_name
        conf_name=$(basename "$conf_file")

        # Determine which HOST ID owns this conf by grepping its hostname var
        # Pattern: HOST1="unRAID-..." or HOST2="unRAID-..."
        local conf_host_id
        conf_host_id=$(grep -m1 -oP '^\s+HOST[0-9]+(?==)' "$conf_file" 2>/dev/null | tr -d ' ')

        if [[ -z "$conf_host_id" ]]; then
            log "Cannot determine HOST ID for $conf_name — including in pull (safe default)"
            continue
        fi

        if [[ "$conf_host_id" != "$MY_ID" ]]; then
            echo "!$conf_name" >> "$sparse_file"
            log "Sparse checkout: excluding $conf_name (belongs to $conf_host_id)"
            ((excluded++))
        else
            log "Sparse checkout: including $conf_name (belongs to $MY_ID — this server)"
        fi
    done

    if [[ "$excluded" -gt 0 ]]; then
        echo "  Sparse checkout: excluding $excluded peer conf file(s) — credentials protected"
    else
        log "Sparse checkout: no peer conf files to exclude (single server or first run)"
    fi
}

# ==============================================================================================
# ━━━ Git Sync ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SYNC Git Sync ━━━"
echo "$ICON_NET  Repo:    $REPO_SSH"
echo "$ICON_GEAR Target:  $TARGET_DIR"
echo ""

START=$(date +%s)
SYNC_SUCCESS=false

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — would sync $REPO_SSH → $TARGET_DIR"
    warn "DRY RUN — would configure sparse checkout for $MY_ID"
    warn "DRY RUN — would exclude peer host*.conf files"
    SYNC_SUCCESS=true
else
    mkdir -p "$TARGET_DIR"
    git config --global --add safe.directory "$TARGET_DIR"
    cd "$TARGET_DIR" || { error "Cannot cd into $TARGET_DIR"; exit 1; }

    if [[ -d ".git" ]]; then
        # ── Existing repository ──────────────────────────────────────────────
        echo "  Existing repository — updating"

        # Configure sparse checkout BEFORE pull
        # Uses conf files already present from last pull to determine exclusions
        configure_sparse_checkout "$TARGET_DIR"

        echo "  Pulling latest changes..."
        if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" git pull --ff-only; then
            echo "  Git pull successful"
            SYNC_SUCCESS=true
        else
            # ff-only fails when local commits or tracked changes exist that can't
            # fast-forward. Fail loudly — never silently destroy local work.
            error "Git pull failed — local changes conflict with remote (will not force-reset)"
            notify "Git pull failed on $(hostname) — local changes conflict, manual resolve needed" "Git Sync" "alert"
            exit 1
        fi

    else
        # ── Fresh clone ──────────────────────────────────────────────────────
        echo "  No repository found — cloning"

        # Clone first — need the repo to exist before configuring sparse checkout
        if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" git clone "$REPO_SSH" .; then
            echo "  Clone successful"

            # Configure sparse checkout after clone
            # Now all host*.conf files are present — can detect exclusions
            configure_sparse_checkout "$TARGET_DIR"

            # Apply sparse checkout — removes excluded files from working tree
            echo "  Applying sparse checkout..."
            git read-tree -mu HEAD
            echo "  Sparse checkout applied — peer credentials removed from working tree"

            SYNC_SUCCESS=true
        else
            error "Clone failed"
            notify "Git clone failed on $(hostname) — check Gitea connectivity" "Git Sync" "alert"
            exit 1
        fi
    fi

    # ── Permissions ──────────────────────────────────────────────────────────
    echo ""
    echo "━━━ $ICON_GEAR Permissions ━━━"
    log "Setting executable permissions on all .sh files..."
    find "$TARGET_DIR" -type f -name "*.sh" -exec chmod +x {} \;
    echo "  Permissions set on .sh files"

    # ── Flash mode: sync Plugin/ to /boot/ so the webUI picks up updates ─────
    # In flash mode SCRIPTS_DIR is in appdata — Plugin/ lives in the repo there
    # but Unraid serves PHP from /boot/. Sync after every pull to keep them in step.
    _BOOT_DIR=$(platform_webui_install_path)
    if [[ "$TARGET_DIR" != "$_BOOT_DIR" ]]; then
        echo ""
        echo "━━━ $ICON_SYNC Flash mode: sync Plugin/ → /boot/ ━━━"
        if rsync -a --delete "$TARGET_DIR/Plugin/" "$_BOOT_DIR/Plugin/" 2>/dev/null; then
            echo "  Plugin/ synced to /boot/ ✅"
        else
            warn "Plugin/ sync to /boot/ failed — webUI may be stale until next pull"
        fi
    fi
fi

END=$(date +%s)

# ==============================================================================================
# ━━━ Conf Upgrade ━━━
# ==============================================================================================
# Merges new conf structure into the live conf files after every pull.
# New keys → added with template defaults (user fills in once).
# Removed keys → dropped. Existing values → always preserved.
# Silent when already up to date — no overhead on unchanged pulls.

echo ""
echo "━━━ $ICON_GEAR Conf Upgrade ━━━"

UPGRADE_SCRIPT="$TARGET_DIR/Deployment/conf_upgrade.sh"
CONF_DIR="$TARGET_DIR/Configurations"
DEPLOY_DIR="$TARGET_DIR/Deployment"

if [[ ! -f "$UPGRADE_SCRIPT" ]]; then
    log "conf_upgrade.sh not found — skipping (pre-deployment-folder repo)"
elif [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — would upgrade master.conf and ${MY_ID,,}.conf"
elif [[ "$SYNC_SUCCESS" == true ]]; then
    _DRY=""

    # master.conf
    if [[ -f "$DEPLOY_DIR/master.conf.template" && -f "$CONF_DIR/master.conf" ]]; then
        bash "$UPGRADE_SCRIPT" \
            --template "$DEPLOY_DIR/master.conf.template" \
            --target   "$CONF_DIR/master.conf" \
            --backup   $_DRY
    else
        warn "master.conf.template or master.conf not found — skipping"
    fi

    # This server's host conf only — sparse checkout ensures we have it
    HOST_CONF="$CONF_DIR/${MY_ID,,}.conf"
    if [[ -f "$DEPLOY_DIR/host.conf.template" && -f "$HOST_CONF" ]]; then
        # Template uses HOSTN_ as generic prefix. Substitute MY_ID before merging
        # so keys match the target (HOST1_* or HOST2_*) and real values are preserved.
        TMPL_RESOLVED=$(mktemp)
        sed "s/HOSTN_/${MY_ID}_/g; s/REMOTE_ID/${REMOTE_ID}/g" "$DEPLOY_DIR/host.conf.template" > "$TMPL_RESOLVED"
        bash "$UPGRADE_SCRIPT" \
            --template "$TMPL_RESOLVED" \
            --target   "$HOST_CONF" \
            --backup   $_DRY
        rm -f "$TMPL_RESOLVED"
    else
        warn "${MY_ID,,}.conf or host.conf.template not found — skipping"
    fi
fi

# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY GIT SYNC SUMMARY ━━━━━"
echo "$ICON_NET  Repo:     $REPO_SSH"
echo "$ICON_GEAR Target:   $TARGET_DIR"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_LOCK Excluded: peer host*.conf files"
echo "$ICON_TIME Duration: $(format_duration $((END - START)))"
if [[ "$DRY_RUN" == true ]]; then
    echo "$ICON_WARN Status:   DRY RUN — no changes made"
elif [[ "$SYNC_SUCCESS" == true ]]; then
    echo "$ICON_DONE Status:   $ICON_SUCCESS DONE"
    notify "Repository synced successfully on $(hostname)" "Git Sync" "normal"
else
    echo "$ICON_ERROR Status:  $ICON_ERROR FAILED"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"