#!/bin/sh
# Portable POSIX /bin/sh — works on bash, dash, busybox ash (OpenWrt).
set -eu

##############################################################################
# bak_cpd — Directory backup with tar + opportunistic multi-threaded compression
# Install: cp bak_cpd /usr/local/sbin/bak_cpd && chmod 755 /usr/local/sbin/bak_cpd
#
# Compressor failover order (best → safest):
#   -z (gz) :  pigz   → gzip  → uncompressed tar
#   -j (bz2):  pbzip2 → lbzip2 → bzip2 → gzip (fallback)
#   -J (xz) :  xz -T  → xz    → gzip (fallback)
##############################################################################

usage() {
    cat << 'USAGE'
Usage: bak_cpd [OPTIONS] <directory>

Options:
  -z                gzip compression (pigz if available)  [default]
  -j                bzip2 compression (pbzip2/lbzip2 if available)
  -J                xz compression (multi-threaded if supported)
  -c                create archive (default, no-op)
  -O <path>         override backup root directory
  --exclude-data    exclude data/ subdirectory from archive

If the requested compressor is missing, falls back to the best available
option and prints a [Warn] line. The archive extension always reflects the
compressor actually used.

Output: <backup_root>/<dirname>.YYYYMMDD-HHMMSS.tar.<ext>
USAGE
    exit 1
}

##############################################################################
# Parse arguments
##############################################################################
COMPRESS=""
OVERRIDE_ROOT=""
EXCLUDE_DATA="false"

while [ $# -gt 0 ]; do
    case "$1" in
        -z) COMPRESS="gz";  shift ;;
        -j) COMPRESS="bz2"; shift ;;
        -J) COMPRESS="xz";  shift ;;
        -c) shift ;;
        -O)
            if [ -z "${2:-}" ]; then
                echo "Error: -O requires a path argument"
                exit 1
            fi
            OVERRIDE_ROOT="$2"
            shift 2
            ;;
        --exclude-data) EXCLUDE_DATA="true"; shift ;;
        -*) echo "Error: unknown option: $1"; usage ;;
        *)  break ;;
    esac
done

[ $# -ne 1 ] && usage
TARGET_DIR="${1%/}"

if [ ! -d "$TARGET_DIR" ]; then
    echo "Error: directory not found: $TARGET_DIR"
    exit 1
fi

[ -z "$COMPRESS" ] && COMPRESS="gz"

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

is_domain_name() {
    case "$1" in
        *.*) return 0 ;;
        *)   return 1 ;;
    esac
}

get_owner() {
    # ls -ld is universally available on busybox/GNU/BSD; stat is missing on
    # minimal busybox builds (e.g. some OpenWrt images).
    ls -ld "$1" 2>/dev/null | awk '{print $3}'
}

get_home_dir() {
    if have getent; then
        getent passwd "$1" | cut -d: -f6
    elif [ -r /etc/passwd ]; then
        awk -F: -v u="$1" '$1==u{print $6; exit}' /etc/passwd
    fi
}

get_nproc() {
    if have nproc; then
        nproc
    elif [ -r /proc/cpuinfo ]; then
        grep -c '^processor' /proc/cpuinfo 2>/dev/null || echo 1
    else
        echo 1
    fi
}

xz_supports_threads() {
    # Full xz: --help advertises -T / threads=. busybox xz typically does not.
    xz --help 2>&1 | grep -qE '(\-T|threads=)'
}

##############################################################################
# Resolve compressor with chain failover.
# Sets: COMPRESS (possibly downgraded), COMPRESSOR (cmd or empty), COMPRESS_EXT
##############################################################################
COMPRESSOR=""
COMPRESS_EXT=""

resolve_compressor() {
    case "$COMPRESS" in
        xz)
            if have xz; then
                if xz_supports_threads; then
                    COMPRESSOR="xz -e -T$(get_nproc)"
                else
                    COMPRESSOR="xz"
                fi
                COMPRESS_EXT="xz"
                return
            fi
            echo "[Warn] xz not found; falling back to gzip"
            COMPRESS="gz"
            ;;
        bz2)
            if have pbzip2; then
                COMPRESSOR="pbzip2"; COMPRESS_EXT="bz2"; return
            elif have lbzip2; then
                COMPRESSOR="lbzip2"; COMPRESS_EXT="bz2"; return
            elif have bzip2; then
                COMPRESSOR="bzip2"; COMPRESS_EXT="bz2"; return
            fi
            echo "[Warn] bzip2/pbzip2/lbzip2 not found; falling back to gzip"
            COMPRESS="gz"
            ;;
    esac

    # gz branch (also reached via xz/bz2 fallback)
    if have pigz; then
        COMPRESSOR="pigz"; COMPRESS_EXT="gz"
    elif have gzip; then
        COMPRESSOR="gzip"; COMPRESS_EXT="gz"
    else
        echo "[Warn] gzip not found; producing uncompressed tar"
        COMPRESSOR=""; COMPRESS_EXT="tar"; COMPRESS="none"
    fi
}

resolve_compressor

##############################################################################
# Resolve backup root directory
##############################################################################
ABS_TARGET="$(cd "$TARGET_DIR" && pwd)"
DIR_NAME="$(basename "$ABS_TARGET")"
PARENT_DIR="$(dirname "$ABS_TARGET")"

resolve_backup_root() {
    if [ -n "$OVERRIDE_ROOT" ]; then
        BACKUP_ROOT="$OVERRIDE_ROOT"
        return
    fi

    owner="$(get_owner "$ABS_TARGET")"
    home_dir="$(get_home_dir "$owner")"

    if [ -z "${home_dir:-}" ] || [ ! -d "${home_dir:-}" ]; then
        home_dir=""
    fi

    BACKUP_ROOT=""
    if [ -n "$home_dir" ]; then
        if [ -d "${home_dir}/backups" ]; then
            BACKUP_ROOT="${home_dir}/backups"
        elif [ -d "${home_dir}/backup" ]; then
            BACKUP_ROOT="${home_dir}/backup"
        fi

        if [ -n "$BACKUP_ROOT" ] && [ -d "${home_dir}/htdocs" ]; then
            has_domain="false"
            for d in "${home_dir}/htdocs"/*/; do
                [ -d "$d" ] || continue
                subdir_name="$(basename "$d")"
                if is_domain_name "$subdir_name"; then
                    has_domain="true"
                    break
                fi
            done

            if [ "$has_domain" = "true" ]; then
                [ -d "${BACKUP_ROOT}/htdocs" ] || mkdir -p "${BACKUP_ROOT}/htdocs"
                BACKUP_ROOT="${BACKUP_ROOT}/htdocs"
            fi
        fi
    fi

    if [ -z "$BACKUP_ROOT" ]; then
        BACKUP_ROOT="${PARENT_DIR}/backups"
    fi
}

resolve_backup_root
mkdir -p "$BACKUP_ROOT"

##############################################################################
# Build archive
##############################################################################
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
if [ "$COMPRESS" = "none" ]; then
    ARCHIVE_NAME="${DIR_NAME}.${TIMESTAMP}.tar"
else
    ARCHIVE_NAME="${DIR_NAME}.${TIMESTAMP}.tar.${COMPRESS_EXT}"
fi
ARCHIVE_PATH="${BACKUP_ROOT}/${ARCHIVE_NAME}"

# Single-string flag instead of bash array; deliberately unquoted in tar call
# so empty value expands to nothing (value never contains spaces).
EXCLUDE_FLAG=""
if [ "$EXCLUDE_DATA" = "true" ]; then
    if [ -d "${ABS_TARGET}/data" ]; then
        EXCLUDE_FLAG="--exclude=./data"
        echo "[Info] Excluding data/ from archive"
    else
        echo "[Info] --exclude-data specified but no data/ directory found in ${ABS_TARGET}"
    fi
fi

if [ -z "$COMPRESSOR" ]; then
    # shellcheck disable=SC2086
    tar -cf "$ARCHIVE_PATH" -C "$PARENT_DIR" $EXCLUDE_FLAG "$DIR_NAME"
else
    # COMPRESSOR may contain space-separated args (e.g. "xz -e -T4"); intentionally unquoted.
    # shellcheck disable=SC2086
    tar -cf - -C "$PARENT_DIR" $EXCLUDE_FLAG "$DIR_NAME" | $COMPRESSOR > "$ARCHIVE_PATH"
fi

# Verify (compensates for missing pipefail: empty file ⇒ pipeline stage failed).
if [ -f "$ARCHIVE_PATH" ] && [ -s "$ARCHIVE_PATH" ]; then
    SIZE="$(du -sh "$ARCHIVE_PATH" | cut -f1)"
    echo "[OK] ${ARCHIVE_PATH} (${SIZE})"
else
    echo "[Error] Failed to create archive"
    rm -f "$ARCHIVE_PATH"
    exit 1
fi
