#!/bin/sh

set -eu

SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)

# ==================== 生产配置区：按服务器实际路径修改 ====================

# JDK 安装目录。留空时使用服务器已有 JAVA_HOME；仍为空时使用 PATH 中的 java。
BACKEND_JAVA_HOME=""

# 后端 Jar 所在目录。生产目录结构为：脚本在外层，Jar 位于 backend/ruoyi-admin.jar。
APP_HOME="${SCRIPT_DIR}/backend"

# 后端 Jar 文件名。
JAR_NAME="ruoyi-admin.jar"

# Spring Profile。
SPRING_PROFILES_ACTIVE="uat"

# JVM 参数。
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"

# 额外应用启动参数，例如：--server.port=8080
APP_ARGS=""

# 停止进程等待秒数。
STOP_WAIT_SECONDS=30

# ==================== 以下为脚本逻辑，一般不需要修改 ====================

if [ "${APP_HOME#/}" = "${APP_HOME}" ]; then
    APP_HOME="${SCRIPT_DIR}/${APP_HOME}"
fi
APP_HOME="${APP_HOME%/}"
JAR_PATH="${APP_HOME}/${JAR_NAME}"
FRONTEND_DIR="${SCRIPT_DIR}/frontend"
LOG_DIR="${APP_HOME}/logs"
CONSOLE_LOG="${LOG_DIR}/backend-console.log"
PID_FILE="${LOG_DIR}/backend-java.pid"
APP_MARKER="-Dccdi.backend.prod.home=${APP_HOME}"
JAVA_CMD="java"
RELATIVE_JAR_PATH=""
TIMESTAMP=$(date '+%Y%m%d%H%M%S')
BACKUP_ROOT="${SCRIPT_DIR}/backups"
BACKUP_DIR="${BACKUP_ROOT}/${TIMESTAMP}"
WORK_ROOT="${SCRIPT_DIR}/.deploy-work"
WORK_DIR="${WORK_ROOT}/release-${TIMESTAMP}"
RELEASE_ZIP=""
RELEASE_JAR=""
RELEASE_DIST_ZIP=""
FRONTEND_SOURCE_DIR=""

case "${APP_HOME}" in
    "${SCRIPT_DIR}"/*)
        RELATIVE_JAR_PATH="${APP_HOME#${SCRIPT_DIR}/}/${JAR_NAME}"
        ;;
esac

timestamp() {
    date '+%Y-%m-%d %H:%M:%S'
}

log_info() {
    printf '[%s] %s\n' "$(timestamp)" "$1"
}

log_error() {
    printf '[%s] %s\n' "$(timestamp)" "$1" >&2
}

usage() {
    cat <<'EOF'
用法: ./ccdi_function.sh [deploy|restart|stop] [上线压缩包路径]

命令:
  deploy   备份 backend/ 与 frontend/，部署上线包，然后重启后端并持续输出日志
  restart  停止旧后端进程，启动 backend/ruoyi-admin.jar，并持续输出日志
  stop     停止当前脚本目录对应的 backend/ruoyi-admin.jar 进程

上线包要求:
  未传上线压缩包路径时，脚本会自动使用当前脚本目录下唯一的 .zip 文件。
  上线压缩包根层必须包含 ruoyi-admin.jar 和 dist.zip。
  dist.zip 解压后必须包含 dist/index.html。

生产目录示例:
  ccdi_function.sh
  backend/
  frontend/
  ccdi_YYYYMMDD.zip

说明:
  restart 和 deploy 启动成功后会持续输出 backend/logs/backend-console.log。
  按 Ctrl+C 仅退出日志查看，不会停止已启动的后端进程。
EOF
}

require_command() {
    if ! command -v "$1" >/dev/null 2>&1; then
        log_error "缺少命令: $1"
        exit 1
    fi
}

resolve_path() {
    input_path="$1"

    case "${input_path}" in
        /*)
            printf '%s\n' "${input_path}"
            ;;
        *)
            input_dir=$(dirname "${input_path}")
            input_base=$(basename "${input_path}")
            printf '%s/%s\n' "$(CDPATH= cd -- "${input_dir}" && pwd)" "${input_base}"
            ;;
    esac
}

resolve_java_cmd() {
    configured_java_home="${BACKEND_JAVA_HOME}"
    if [ -z "${configured_java_home}" ]; then
        configured_java_home="${JAVA_HOME:-}"
    fi

    if [ -n "${configured_java_home}" ]; then
        configured_java_home="${configured_java_home%/}"
        if [ ! -x "${configured_java_home}/bin/java" ]; then
            log_error "配置的 JAVA_HOME 无效，未找到可执行文件: ${configured_java_home}/bin/java"
            exit 1
        fi

        JAVA_HOME="${configured_java_home}"
        export JAVA_HOME
        JAVA_CMD="${JAVA_HOME}/bin/java"
    else
        require_command "java"
        JAVA_CMD="java"
    fi

    log_info "使用 Java 命令: ${JAVA_CMD}"
}

get_process_table() {
    if ! ps -ef 2>/dev/null; then
        log_error "执行 ps -ef 失败，无法扫描旧进程"
        return 1
    fi
}

is_backend_process_line() {
    process_line="$1"

    case "${process_line}" in
        *"<defunct>"*)
            return 1
            ;;
        *" -jar ${JAR_PATH}"*)
            return 0
            ;;
        *"${APP_MARKER}"*)
            return 0
            ;;
    esac

    if [ -n "${RELATIVE_JAR_PATH}" ]; then
        case "${process_line}" in
            *" -jar ${RELATIVE_JAR_PATH}"*)
                return 0
                ;;
        esac
    fi

    return 1
}

is_managed_pid() {
    check_pid="$1"
    if [ -z "${check_pid}" ] || ! kill -0 "${check_pid}" 2>/dev/null; then
        return 1
    fi

    if ! process_table=$(get_process_table); then
        return 1
    fi

    while IFS= read -r process_line; do
        set -- ${process_line}
        line_pid="${2:-}"
        if [ "${line_pid}" = "${check_pid}" ] && is_backend_process_line "${process_line}"; then
            return 0
        fi
    done <<EOF
${process_table}
EOF

    return 1
}

collect_pids() {
    all_pids=""
    unique_pids=""

    if ! process_table=$(get_process_table); then
        return 1
    fi

    if [ -f "${PID_FILE}" ]; then
        pid_file_value=$(sed -n '1p' "${PID_FILE}" 2>/dev/null || true)
        if is_managed_pid "${pid_file_value}"; then
            all_pids="${all_pids} ${pid_file_value}"
        fi
    fi

    while IFS= read -r process_line; do
        set -- ${process_line}
        scan_pid="${2:-}"
        case "${scan_pid}" in
            ''|*[!0-9]*)
                continue
                ;;
        esac

        if is_backend_process_line "${process_line}"; then
            all_pids="${all_pids} ${scan_pid}"
        fi
    done <<EOF
${process_table}
EOF

    for scan_pid in ${all_pids}; do
        case " ${unique_pids} " in
            *" ${scan_pid} "*)
                ;;
            *)
                unique_pids="${unique_pids} ${scan_pid}"
                ;;
        esac
    done

    printf '%s\n' "${unique_pids}" | xargs 2>/dev/null || true
}

start_backend() {
    resolve_java_cmd

    if [ ! -f "${JAR_PATH}" ]; then
        log_error "未找到后端 Jar: ${JAR_PATH}"
        exit 1
    fi

    if ! running_pids=$(collect_pids); then
        log_error "扫描后端进程失败"
        exit 1
    fi
    if [ -n "${running_pids}" ]; then
        log_error "检测到后端已在运行: ${running_pids}"
        exit 1
    fi

    mkdir -p "${LOG_DIR}"
    printf '\n===== %s start =====\n' "$(timestamp)" >>"${CONSOLE_LOG}"

    profile_arg=""
    if [ -n "${SPRING_PROFILES_ACTIVE}" ]; then
        profile_arg="--spring.profiles.active=${SPRING_PROFILES_ACTIVE}"
    fi

    log_info "开始启动后端 Jar: ${JAR_PATH}"
    nohup "${JAVA_CMD}" "${APP_MARKER}" ${JAVA_OPTS} -jar "${JAR_PATH}" ${profile_arg} ${APP_ARGS} >>"${CONSOLE_LOG}" 2>&1 &
    echo $! >"${PID_FILE}"

    sleep 3

    starter_pid=$(sed -n '1p' "${PID_FILE}" 2>/dev/null || true)
    if [ -z "${starter_pid}" ] || ! kill -0 "${starter_pid}" 2>/dev/null; then
        log_error "启动命令未保持运行，请检查日志: ${CONSOLE_LOG}"
        exit 1
    fi

    log_info "后端启动完成，PID: ${starter_pid}"
}

stop_backend() {
    if ! pids=$(collect_pids); then
        log_error "扫描后端进程失败"
        exit 1
    fi

    if [ -z "${pids}" ]; then
        log_info "未发现运行中的后端进程"
        rm -f "${PID_FILE}"
        return 0
    fi

    log_info "准备停止后端进程: ${pids}"
    for pid in ${pids}; do
        kill -TERM "${pid}" 2>/dev/null || true
    done

    elapsed=0
    remaining_pids="${pids}"
    while [ -n "${remaining_pids}" ] && [ "${elapsed}" -lt "${STOP_WAIT_SECONDS}" ]; do
        sleep 1
        elapsed=$((elapsed + 1))
        remaining_pids=""
        for pid in ${pids}; do
            if kill -0 "${pid}" 2>/dev/null; then
                remaining_pids="${remaining_pids} ${pid}"
            fi
        done
        remaining_pids=$(printf '%s\n' "${remaining_pids}" | xargs 2>/dev/null || true)
    done

    if [ -n "${remaining_pids}" ]; then
        log_info "仍有进程未退出，执行强制停止: ${remaining_pids}"
        for pid in ${remaining_pids}; do
            kill -KILL "${pid}" 2>/dev/null || true
        done
    fi

    rm -f "${PID_FILE}"
    log_info "后端停止完成"
}

follow_logs() {
    require_command "tail"

    mkdir -p "${LOG_DIR}"
    touch "${CONSOLE_LOG}"
    log_info "持续输出日志中，按 Ctrl+C 仅退出日志查看，不会停止后端进程"
    tail -n 200 -F "${CONSOLE_LOG}"
}

restart_action() {
    stop_backend
    start_backend
    follow_logs
}

resolve_release_zip() {
    if [ -n "${RELEASE_ZIP}" ]; then
        RELEASE_ZIP=$(resolve_path "${RELEASE_ZIP}")
    else
        mkdir -p "${WORK_ROOT}"
        candidate_file="${WORK_ROOT}/zip-candidates-${TIMESTAMP}.txt"
        find "${SCRIPT_DIR}" -maxdepth 1 -type f -name '*.zip' ! -name 'dist.zip' | sort >"${candidate_file}"
        candidate_count=$(wc -l <"${candidate_file}" | tr -d ' ')

        if [ "${candidate_count}" -eq 0 ]; then
            log_error "未在脚本目录找到上线压缩包，请传入压缩包路径"
            exit 1
        fi

        if [ "${candidate_count}" -gt 1 ]; then
            log_error "脚本目录存在多个上线压缩包，请显式传入压缩包路径"
            cat "${candidate_file}" >&2
            exit 1
        fi

        RELEASE_ZIP=$(sed -n '1p' "${candidate_file}")
        rm -f "${candidate_file}"
    fi

    if [ ! -f "${RELEASE_ZIP}" ]; then
        log_error "上线压缩包不存在: ${RELEASE_ZIP}"
        exit 1
    fi
}

assert_deploy_layout() {
    if [ ! -d "${APP_HOME}" ]; then
        log_error "未找到后端目录: ${APP_HOME}"
        exit 1
    fi

    if [ ! -d "${FRONTEND_DIR}" ]; then
        log_error "未找到前端目录: ${FRONTEND_DIR}"
        exit 1
    fi
}

backup_dir() {
    source_dir="$1"
    target_dir="$2"

    mkdir -p "${target_dir}"
    if [ -n "$(find "${source_dir}" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
        cp -a "${source_dir}/." "${target_dir}/"
        log_info "已备份 ${source_dir} 到 ${target_dir}"
    else
        log_info "目录为空，已创建空备份目录: ${target_dir}"
    fi
}

backup_current_files() {
    mkdir -p "${BACKUP_DIR}"
    backup_dir "${APP_HOME}" "${BACKUP_DIR}/backend"
    backup_dir "${FRONTEND_DIR}" "${BACKUP_DIR}/frontend"
}

extract_release_package() {
    mkdir -p "${WORK_DIR}/release" "${WORK_DIR}/frontend"
    log_info "开始解压上线压缩包: ${RELEASE_ZIP}"
    unzip -q "${RELEASE_ZIP}" -d "${WORK_DIR}/release"

    RELEASE_JAR="${WORK_DIR}/release/${JAR_NAME}"
    RELEASE_DIST_ZIP="${WORK_DIR}/release/dist.zip"
    if [ ! -f "${RELEASE_JAR}" ]; then
        log_error "上线压缩包根层缺少 ${JAR_NAME}"
        exit 1
    fi
    if [ ! -f "${RELEASE_DIST_ZIP}" ]; then
        log_error "上线压缩包根层缺少 dist.zip"
        exit 1
    fi

    unzip -q "${RELEASE_DIST_ZIP}" -d "${WORK_DIR}/frontend"
    FRONTEND_SOURCE_DIR="${WORK_DIR}/frontend/dist"
    if [ ! -f "${FRONTEND_SOURCE_DIR}/index.html" ]; then
        log_error "dist.zip 解压后未找到 dist/index.html"
        exit 1
    fi
}

deploy_backend() {
    target_jar="${APP_HOME}/${JAR_NAME}"
    deploying_jar="${APP_HOME}/.${JAR_NAME}.deploying"

    cp "${RELEASE_JAR}" "${deploying_jar}"
    mv "${deploying_jar}" "${target_jar}"
    log_info "后端 Jar 已部署: ${target_jar}"
}

deploy_frontend() {
    find "${FRONTEND_DIR}" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
    cp -a "${FRONTEND_SOURCE_DIR}/." "${FRONTEND_DIR}/"
    log_info "前端文件已部署到: ${FRONTEND_DIR}"
}

cleanup_work_dir() {
    rm -rf "${WORK_DIR}"
}

deploy_action() {
    RELEASE_ZIP="${1:-}"

    require_command "find"
    require_command "unzip"

    resolve_release_zip
    assert_deploy_layout
    backup_current_files

    trap cleanup_work_dir 0
    extract_release_package
    deploy_backend
    deploy_frontend
    cleanup_work_dir
    trap - 0

    log_info "部署完成，备份目录: ${BACKUP_DIR}"
    log_info "开始重启后端并输出日志"
    restart_action
}

main() {
    action="${1:-help}"
    case "${action}" in
        deploy)
            shift
            if [ "$#" -gt 1 ]; then
                usage
                exit 1
            fi
            deploy_action "${1:-}"
            ;;
        restart)
            shift
            if [ "$#" -ne 0 ]; then
                usage
                exit 1
            fi
            restart_action
            ;;
        stop)
            shift
            if [ "$#" -ne 0 ]; then
                usage
                exit 1
            fi
            stop_backend
            ;;
        -h|--help|help)
            usage
            ;;
        *)
            usage
            exit 1
            ;;
    esac
}

main "$@"
