#!/bin/bash

source $1
export BACKUP_DIR=/tmp
export LOG_FILE=/var/log/whatap_update.log
export PATCH_RESULT_FILE="$INSTALL_DIR/patch_history.log"
# SERVER-1985: PATCH_MARKER_WRITTEN goes to 1 once the in-progress marker is on
# disk. Before that there is nothing to close out and PATCH_VERSION /
# PATCH_START_TIME are not set yet, so error_exit has to stay quiet.
# PATCH_RESULT_WRITTEN goes to 1 once any final record lands, so a later
# error_exit cannot append a second verdict on top of it.
PATCH_MARKER_WRITTEN=0
PATCH_RESULT_WRITTEN=0

#pkg="$1"
if [[ -z "${pkg:-}" ]]; then
	echo "usage: $0 <pkg-file>"
	exit 2
fi

os_id="" ; os_like=""
if [[ -r /etc/os-release ]]; then
	os_id=$(awk -F= '$1=="ID"{gsub(/"/,"");print tolower($2)}' /etc/os-release)
	os_like=$(awk -F= '$1=="ID_LIKE"{gsub(/"/,"");print tolower($2)}' /etc/os-release)
fi

is_like() {
	[[ "$os_id" == *"$1"* ]] || [[ "$os_like" == *"$1"* ]]
}

# Logging function
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# Error handling
error_exit() {
    log "ERROR: $1"
    # SERVER-1985: close out the in-progress marker before bailing. Left open it
    # stays the last line of patch_history.log forever, the agent keeps answering
    # "in progress", and OPM can only give up through max_check_count with a
    # generic timeout - throwing away the reason we already have right here.
    # Reachable from backup_whatap / rollback_whatap, both of which run after
    # the marker is written.
    if [[ "$PATCH_MARKER_WRITTEN" == "1" && "$PATCH_RESULT_WRITTEN" == "0" ]]; then
        write_patch_result "failed" "$PATCH_VERSION" "$PATCH_START_TIME" "$1"
    fi
    exit 1
}

error() {
    log "ERROR: $1"
}

# Patch result logging functions
write_patch_result() {
    local status="$1"
    local patch_version="$2"
    local start_time="$3"
    local error_message="$4"
    local end_time=$(date '+%Y-%m-%d %H:%M:%S')

    # SERVER-1985: rollback_whatap clears $INSTALL_DIR before restoring it, so a
    # rollback that dies half-way can leave the directory holding
    # patch_history.log missing. Recreate it rather than lose the record.
    local result_dir=$(dirname "$PATCH_RESULT_FILE")
    [[ -d "$result_dir" ]] || mkdir -p "$result_dir" 2>/dev/null

    # Format: status|patch_version|start_time|end_time|error_message
    if echo "$status|$patch_version|$start_time|$end_time|$error_message" >> "$PATCH_RESULT_FILE"; then
        PATCH_RESULT_WRITTEN=1
        log "Patch result written to $PATCH_RESULT_FILE"
    else
        log "ERROR: failed to write patch result to $PATCH_RESULT_FILE"
    fi
}

# SERVER-1985: append an in-progress marker at patch start so that the last
# line of patch_history.log always belongs to the current patch. Without it,
# a status query arriving before the final result is written would read the
# previous patch result (e.g. report success for a failing patch), and the
# same happens if the patch process dies mid-way. The agent treats the
# "inprogress" status as "no final result yet" and OPM keeps polling.
# Same record format, with end_time/error_message empty.
write_patch_inprogress() {
    if echo "inprogress|$PATCH_VERSION|$PATCH_START_TIME||" >> "$PATCH_RESULT_FILE"; then
        PATCH_MARKER_WRITTEN=1
        log "Patch in-progress marker written to $PATCH_RESULT_FILE"
    else
        # Nothing else in this script depends on the marker, so keep patching -
        # but say so loudly: without it a status query during this patch reads
        # the previous result again, which is what the marker exists to prevent.
        log "ERROR: failed to write in-progress marker to $PATCH_RESULT_FILE; a status query during this patch may report the previous patch result"
    fi
}

get_patch_version() {
    local pkg_file="$1"
    local version=""

    # Extract version from package
    if [[ "$pkg_type" == "tar" ]]; then
        # Read conf/VERSION from tarball without full extraction
        version=$(tar -xzf "$pkg_file" -O "whatap-infra/conf/VERSION" 2>/dev/null | head -1 | tr -d '[:space:]')
        [[ -z "$version" ]] && version=$(basename "$pkg_file" | sed -E 's/.*-([0-9]+\.[0-9]+\.[0-9]+)-.*/\1/')
        [[ -z "$version" ]] && version="unknown"
    elif [[ "$pkg_type" == "deb" ]] && need dpkg-deb; then
        version=$(dpkg-deb --field "$pkg_file" Version 2>/dev/null || echo "unknown")
    elif [[ "$pkg_type" == "rpm" ]] && need rpm; then
        version=$(rpm -qp --qf '%{VERSION}.%{RELEASE}\n' "$pkg_file" 2>/dev/null || echo "unknown")
    else
        # Fallback: extract from filename
	version=$(basename "$pkg_file" | sed -E 's/.*-([0-9]+\.[0-9]+)-([0-9]+).*/\1.\2/; t; s/.*_([0-9]+\.[0-9]+\.[0-9]+)_.*/\1/')
        [[ -z "$version" ]] && version=$(basename "$pkg_file")
    fi

    echo "$version"
}


stop_service() {
    log "service stop..."
    service whatap-infra stop
    if [[ $? -ne 0 ]]; then 
        log "service stop error"
    fi
    log "service stop ok"
}

start_service() {
    log "service start..."
    rm -rf $INSTALL_DIR/conf/whatap_infrad.pid
    service whatap-infra start
    if [[ $? -ne 0 ]]; then 
        log "service start error"
    fi
    log "service start ok"
}

backup_whatap() {
    log "creating backup of existing installation..."
    if [[ -d "$INSTALL_DIR" ]]; then
        mkdir -p "$BACKUP_DIR"
        cp -r "$INSTALL_DIR" "$BACKUP_DIR/" || error_exit "failed to create backup"
        log "backup created at $BACKUP_DIR"
    else
        log "no existing installation found to backup"
    fi
    log "backup ok"
}


# SERVER-1985: rollback_whatap no longer starts the service itself. The
# caller writes the patch result first (after the restore, because the
# restore wipes $INSTALL_DIR and would delete a record written earlier) and
# only then calls start_service, so the agent never comes up before the
# final result is recorded.
rollback_whatap() {
    log "rollback start..."
    stop_service
    rm -rf "$INSTALL_DIR"
    cp -r "$BACKUP_DIR"/infra "$INSTALL_DIR" || error_exit "failed to rollback"
    echo " rollback ok"
}

need() { command -v "$1" >/dev/null 2>&1; }

# Extract RPM --prefix from INSTALL_DIR
# e.g., /usr/whatap/infra -> /usr/whatap, /opt/whatap/infra/ -> /opt/whatap, /opt/infra -> /opt
get_rpm_prefix() {
	local install_dir="${INSTALL_DIR%/}"
	local prefix="${install_dir%/infra}"
	if [[ -z "$prefix" ]]; then
		prefix="/usr/whatap"
	fi
	echo "$prefix"
}

install_deb_file() {
	echo "install $pkg..."
	if need dpkg; then
		sudo dpkg -i "$pkg"
	else
		error "dpkg command not found"
		PATCH_FAIL_MSG="dpkg command not found"
		return 1
	fi

	if [[ $? -ne 0 ]]; then
		error "$pkg install fail"
		PATCH_FAIL_MSG="deb package install failed"
		return 1
	else
		log "$pkg install ok"
		return 0
	fi
}

install_rpm_file() {
	local rpm_prefix
	rpm_prefix=$(get_rpm_prefix)
	log "install $pkg with --prefix=$rpm_prefix ..."
	if need rpm; then
		sudo rpm -Uvh --replacepkgs --oldpackage --prefix="$rpm_prefix" "$pkg"
	elif need dnf; then
		sudo dnf -y install "$pkg"
	elif need yum; then
		sudo yum -y localinstall "$pkg" || sudo rpm -Uvh --replacepkgs --oldpackage "$pkg"
	elif need zypper; then
		sudo zypper --non-interactive install "$pkg"
	else
		error "rpm command not found"
		PATCH_FAIL_MSG="rpm command not found"
		return 1
	fi

	if [[ $? -ne 0 ]]; then
		error "$pkg install fail"
		PATCH_FAIL_MSG="rpm package install failed"
		return 1
	else
		log "$pkg install ok"
		return 0
	fi
	return $?
}

install_tar_file() {
	local tmp_dir
	tmp_dir=$(mktemp -d) || { error "failed to create temp directory"; PATCH_FAIL_MSG="failed to create temp directory"; return 1; }
	trap 'rm -rf "$tmp_dir"' RETURN

	log "extracting $pkg to $tmp_dir ..."
	if ! tar -xzf "$pkg" -C "$tmp_dir"; then
		error "failed to extract tarball: $pkg"
		PATCH_FAIL_MSG="Package extract failed"
		return 1
	fi

	# Find install.sh inside extracted directory
	local install_script=""
	if [[ -f "$tmp_dir/whatap-infra/install.sh" ]]; then
		install_script="$tmp_dir/whatap-infra/install.sh"
	else
		# Search for install.sh in extracted contents
		install_script=$(find "$tmp_dir" -maxdepth 2 -name "install.sh" -type f | head -1)
	fi

	if [[ -z "$install_script" || ! -f "$install_script" ]]; then
		error "install.sh not found in tarball"
		PATCH_FAIL_MSG="install.sh not found in tarball"
		return 1
	fi

	chmod +x "$install_script"
	log "running $install_script --prefix $INSTALL_DIR ..."
	if ! bash "$install_script" --prefix "$INSTALL_DIR"; then
		error "install.sh execution failed"
		PATCH_FAIL_MSG="install.sh execution failed"
		return 1
	fi

	log "$pkg tarball install ok"
	return 0
}

pkg_type=""
pkg_arch=""
detect_pkg_type() {
	if [[ -f "$pkg" ]]; then
		# Check tarball first by extension
		case "$pkg" in
			*.tar.gz|*.tgz)
				pkg_type="tar"
				pkg_arch=$(uname -m)
				return
				;;
		esac
		if need dpkg-deb && dpkg-deb --info "$pkg" >/dev/null 2>&1; then
			pkg_type="deb"
			pkg_arch=$(dpkg-deb --field "$pkg" Architecture || true)
			return
		fi
		if need rpm && rpm -qp --qf '%{NAME}\n' "$pkg" >/dev/null 2>&1; then
			pkg_type="rpm"
			pkg_arch=$(rpm -qp --qf '%{ARCH}\n' "$pkg" || true)
			return
		fi
		case "$(file -b "$pkg" | tr '[:upper:]' '[:lower:]')" in
			*'debian binary package'*) pkg_type="deb" ;;
			*'rpm'*) pkg_type="rpm" ;;
			*'gzip compressed'*|*'tar archive'*) pkg_type="tar" ; pkg_arch=$(uname -m) ;;
		esac
	fi
}


install_package() {
	echo "install package..."
	# SERVER-1985: package/OS mismatch used to exit 1 directly, which would
	# leave the in-progress marker as the last record forever. Return 1 with a
	# distinct PATCH_FAIL_MSG instead so the caller records the result.
	# Tarball is OS-independent, handle first
	if [[ "$pkg_type" == "tar" ]]; then
		install_tar_file
	elif is_like "debian" || is_like "ubuntu" || need apt-get; then
		[[ "$pkg_type" == "deb" ]] || { error "deb-based OS but file is not .deb"; PATCH_FAIL_MSG="deb-based OS but package is not .deb"; return 1; }
		install_deb_file
	elif is_like "suse" || need zypper; then
		[[ "$pkg_type" == "rpm" ]] || { error "suse-based OS but file is not .rpm"; PATCH_FAIL_MSG="suse-based OS but package is not .rpm"; return 1; }
		install_rpm_file
	elif is_like "rhel" || is_like "centos" || is_like "fedora" || is_like "alma" || is_like "rocky" || is_like "amzn" || need dnf || need yum; then
		[[ "$pkg_type" == "rpm" ]] || { error "rpm-based OS but file is not .rpm"; PATCH_FAIL_MSG="rpm-based OS but package is not .rpm"; return 1; }
		install_rpm_file
	else
		error "unsupported distro for local file install"
		PATCH_FAIL_MSG="unsupported distro for local file install"
		return 1
	fi

	if [[ $? -ne 0 ]]; then
		error "install package error"
		return 1
	else
		log "install package ok"
		return 0
	fi
}

test_agent() {
	log "testing agent..."

	# SERVER-1985: each failure point sets a distinct PATCH_FAIL_MSG so
	# patch_history.log identifies where the test failed. The first two
	# failures happen before boot-test even runs, so a generic message made
	# the boot-test log (still holding the previous success) look
	# contradictory and delayed root-cause analysis.
	if [[ -x "$INSTALL_DIR/whatap_infrad" ]]; then
		"$INSTALL_DIR/whatap_infrad" version
		if [[ $? -ne 0 ]]; then
			error "Agent Version Check Failed"
			PATCH_FAIL_MSG="Agent version check failed (boot-test not executed)"
			return 1
		fi
        export WHATAP_HOME="$INSTALL_DIR/conf"
		"$INSTALL_DIR/whatap_infrad" boot-test
		if [[ $? -ne 0 ]]; then
			error "Agent Test Check Failed"
			PATCH_FAIL_MSG="Agent boot-test failed"
			return 1
		fi
		return 0
	else
		error "agent test fail"
		PATCH_FAIL_MSG="Agent binary missing or not executable (boot-test not executed)"
		return 1
	fi
}

check_root() {
    log "checking root privileges..."
    if [[ $EUID -ne 0 ]]; then
        error_exit "This script must be run as root"
    fi
    log "check root ok"
}


check_root


detect_pkg_type
PATCH_START_TIME=$(date '+%Y-%m-%d %H:%M:%S')
PATCH_VERSION=$(get_patch_version "$pkg")
PATCH_FAIL_MSG=''
log "Starting patch process for version: $PATCH_VERSION at $PATCH_START_TIME"

write_patch_inprogress

backup_whatap
stop_service

###
install_package
# SERVER-1985: write_patch_result is called before start_service in every
# path. The outcome is already final at that point, and recording it first
# closes the window in which a restarted agent would answer a status query
# with the previous patch result. On the rollback paths the result is written
# after rollback_whatap (the restore wipes $INSTALL_DIR, so an earlier write
# would be lost) but still before start_service; the in-progress marker
# (included in the backup) covers the interval until then. start_service
# failures were never reflected in patch_history before either (log only),
# so no behavior is lost.
if [[ $? -ne 0 ]]; then
	# 패키지 설치 실패 - 롤백
	[[ -z "$PATCH_FAIL_MSG" ]] && PATCH_FAIL_MSG="Package installation failed"
	log "Package installation failed - rollback"
	rollback_whatap
	write_patch_result "rollback" "$PATCH_VERSION" "$PATCH_START_TIME" "$PATCH_FAIL_MSG"
	start_service
else
	log "install complete"
	test_agent
	if [[ $? -ne 0 ]]; then
		# 에이전트 테스트 실패 - 롤백
		[[ -z "$PATCH_FAIL_MSG" ]] && PATCH_FAIL_MSG="Agent test failed"
		log "Agent test failed - rollback"
		rollback_whatap
		write_patch_result "rollback" "$PATCH_VERSION" "$PATCH_START_TIME" "$PATCH_FAIL_MSG"
		start_service
	else
        # 성공
		write_patch_result "success" "$PATCH_VERSION" "$PATCH_START_TIME" ""
        start_service
        log "Patch completed successfully"
	fi
fi


