#!/bin/bash
el_c_n="\033[0;39m" # Normal color  (orig: normal)
el_c_r="\033[1;31m" # Red color  (orig: red)
el_c_r2="\033[0;31m" # Red2 color  (orig: red)
el_c_g="\033[1;32m" # Green color  (orig: green)
el_c_g2="\033[0;32m" # Green2 color  (orig. green2)
el_c_y="\033[1;33m" # Yellow color  (orig. yellow)
el_c_y2="\033[0;33m" # Yellow2 color  (orig. yellow)
el_c_m="\033[1;35m" # Magenta color
el_c_m2="\033[0;35m" # Magenta2 color
el_c_c="\033[1;36m" # Cyan color
el_c_c2="\033[0;36m" # Cyan2 color
el_c_b="\033[1;34m" # Blue color
el_c_b2="\033[0;34m" # Blue2 color
el_c_gr="\033[1;30m" # Gray color
el_c_gr2="\033[0;30m" # Gray2 color

el_c_blink="\033[5m" # Blink 'color' effect  (orig. blink)
el_c_underline="\033[4m" # Underline 'color' effect  (orig. underline)
el_c_italic="\033[3m" # Italic 'color' effect
el_c_bold="\033[1m" # Bold 'color' effect

: "${EL_DEBUG:="0"}" # debug default level

set -e

# if we have the original and updated functions, use them (so: we are in elive)
if [[ -s /usr/lib/elive-tools/functions ]] ; then
    source /usr/lib/elive-tools/functions
    el_make_environment
fi

case "$( cat /etc/debian_version )" in
    13.*|"trixie"*)
        rdiff_opts="$rdiff_opts --api-version 201"
        ;;
    12.*|"bookworm"*)
        true
        ;;
    11.*|"bullseye"*)
        is_old_rdiff=1
        ;;
    10.*|9.*|8.*)
        is_old_rdiff=1
        ;;
esac

if ! ((is_old_rdiff)) ; then
    rdiff_opts="$rdiff_opts --terminal-verbosity 2"
fi


check_files_owner_compatibility(){
    local dir
    # check for files that are not owned by you so it can cause problems using this tool
    while read -ru 3 dir
    do
        if [[ -d "$dir" ]] ; then
            el_debug "checking for files not owned by you in '$dir'"

            if [[ "$( find "$dir" ! -user "$USER" 2>/dev/null | wc -l )" -gt 0 ]] && [[ "$UID" != 0 ]] ; then
                echo ""
                echo ""
                el_error "These files can give problems:"
                find "$dir" ! -user "$USER" 2>/dev/null

                echo ""
                EL_DEBUG=2 el_warning "Files owned by someone else found"
                el_info "We suggest to configure your sudo to NOPASSWD so this tool can use it, this is important because you are trying to deal with a directory that has files owned not by you and this can cause problems like owners changed on these files when restoring it"
                el_info "Configuration example: add your user to the sudo group and an entry to your /etc/sudoers like:"
                #el_info "$USER ALL=NOPASSWD: /usr/bin/apt-get install"
                #el_info "$USER ALL=NOPASSWD: /usr/bin/apt-get*"
                el_info "$USER ALL=NOPASSWD: /usr/bin/bkp"
                echo ""

                # we have informed so we can finish here
                break
            fi
        fi
    done 3<<< "$(printf "%s\n" "$@")"
}

do_sudo(){
    # run something as sudo, but if fails warn it
    if ! $SUDO_MODE "$@" ; then
        el_error "Unable to run \"$*\""
        el_info "this could be caused by lack of permissions in the files, a solution is to configured a NOPASSWD mode of sudo for your user and this will will use it"
        return 1
    else
        return 0
    fi
}

# extract version timestamps from rdiff-backup listing, handles old and new API formats
_rdiff_list_timestamps() {
    local path="$1"
    local output firstline
    if ((is_old_rdiff)) ; then
        output="$(do_sudo rdiff-backup --parsable-output -l "$path" 2>/dev/null)" || return 1
        awk '{print $1}' <<< "$output"
    else
        output="$(do_sudo rdiff-backup $rdiff_opts --parsable-output list increments "$path" 2>/dev/null)" || return 1
        # get first line
        firstline="${output%%$'\n'*}"
        if [[ "$firstline" = "---" ]]; then
            # new API format (YAML)
            grep '^  time:' <<< "$output" | sed 's/.*: //'
        else
            # old API fallback
            awk '{print $1}' <<< "$output"
        fi
    fi
}

do_work(){
    # this feature does a temporal copy of the directory for work from it,
    # everything is erased after the logout
    local dir tmpdir

    dir="$( readlink -f "$(pwd)" )"
    tmpdir="${bkpdir_works}/${namedir}"

    do_sudo rm -rf "${tmpdir}"
    mkdir -p "${tmpdir}"
    rmdir "${tmpdir}" # remove last dir in order to really-copy here

    cp -aL "${dir}" "${tmpdir}"

    cd "${tmpdir}"

    echo -e "You are now in a sandbox copy where you can modify safely these files"
    echo -e "Your modifications will be merged or erased after you finish"
    el_explain 0 "logout from the shell for finish your working state"

    # entering in new shell
    PROMPT_PREPEND="work" $SHELL || true
    # go back
    cd -P "$target"

    if el_confirm "Do you want to run meld with your previous working state?" ; then
        if ! el_dependencies_check meld ; then
            if ! el_dependencies_install meld ; then
                el_error "Install required dependencies"
            fi
        fi
        meld "${tmpdir}" .
    fi
    do_sudo rm -rf "${tmpdir}"

}

do_purge_olds(){
    local others_list_num

    others_list_num="$( ls -1 "${bkpdir_backups}" 2>/dev/null | wc -l )"

    # remove backups
    if [[ -d "${bkpdir_backups}" ]] ; then
        do_sudo rm -rf "${bkpdir_backups}/%"*
    fi
    # remove working states
    if [[ -d "${bkpdir_works}" ]] ; then
        do_sudo rm -rf "${bkpdir_works}/%"*
    fi


    echo -e "${el_c_y}All backups and workstates removed${el_c_n}  [${others_list_num}]"
}


do_snap(){
    local target
    target="${1%/}"
    shift

    if ! [[ "$( df --output=fstype . | tail -n 1 )" = "btrfs" ]] ; then
        NOREPORTS=1  el_error "This feature only works with BTRFS, so it requires COW to work."
        return
    fi

    if [[ -d "${target}.snapshot" ]] ; then
        # NOREPORTS=1 el_warning "directory ${target}.snapshot already exists"
        el_info "Snapshot already exist in '${target}.snapshot', doing a new snapshot..."
        # if el_confirm "Do you want to remove it?" ; then
            $SUDO_MODE rm -rf "${target}.snapshot"
        # else
            # NOREPORTS=1 el_error "Aborted"
            # exit
        # fi
    fi

    if ! cp -aL --reflink=always "${target}" "${target}.snapshot" ; then
        NOREPORTS=1 el_error "Failed to create snapshot"
    fi

    if [[ "$PROMPT_PREPEND" = "snapshot" ]] ; then
        # finish here because we come from a previous subshell
        el_info "You are already in a snapshot shell, exit it to finish your snapshot state"
        return 0
    else
        el_info "Snapshot created, exit your SHELL when you finish"
        PROMPT_PREPEND="snapshot" $SHELL -l || true
    fi

    if el_confirm "Recover FROM its snapshoted state?" ; then
        #$SUDO_MODE rsync -a --delete --exclude="build/*" --info=flist0,name "${target}.snapshot/" "${target}/" | grep -v "^\.git/$"
        #$SUDO_MODE find "${target}.snapshot" -mindepth 1 -maxdepth 1 -exec mv -t "${target}/" "{}" \;
        $SUDO_MODE bash -c '
            shopt -s dotglob nullglob
            target="$1"
            if [[ ! -d "$target.snapshot" ]]; then
                echo "Error: Source directory $target.snapshot missing" >&2
                exit 1
            fi
            while read -ru 3 item
            do
                dest="$target/$(basename "$item")"
                [[ -e "$dest" ]] && rm -rf "$dest"
                mv -f "$item" "$target/" || exit 1
            done 3<<< "$(printf "%s\n" "$target.snapshot"/*)"
            shopt -u dotglob nullglob
        ' _ "$target"
    fi

    $SUDO_MODE rm -rf "${target}.snapshot"
    el_info "Snapshot removed"

}

do_save(){
    local version others_list_num ref
    local src="${1%/}"
    local dst="${2%/}"

    mkdir -p "${bkpdir_backups}"

    # show old existing backups
    if ! ((is_quiet)) ; then
        others_list_num="$( ls -1 "${bkpdir_backups}" 2>/dev/null | grep "\.rdiff$" | wc -l )"
        if [[ "$others_list_num" -gt 5 ]] ; then
            echo -e "Warning: other backups found:"
            while read -ru 3 line
            do
               ref="${line//%//}"
               ref="${ref%.rdiff*}"
               if [[ -d "${ref}" ]] ; then
                   echo -e " * ref: ${el_c_c}${ref}${el_c_n}"
               else
                   echo -e " * ref: ${el_c_r2}${ref}${el_c_n}"
               fi

            done 3<<< "$( ls -1 "${bkpdir_backups}" | grep "\.rdiff$" )"

            echo -e ""
            echo -e "To remove single entry's just enter in the dir and run: ${el_c_g}$(basename $0) ${el_c_m}remove${el_c_n}"
            echo -e "You can remove ALL in one shot with: ${el_c_g}$(basename $0) ${el_c_y}purge${el_c_n}\n"
        fi
    fi

    if ! [[ -d "$bkpdir_id" ]] ; then
        mkdir -p "$bkpdir_id"
    fi

    if ! ((is_quiet)) ; then
        echo -ne "Saving..."
    fi

    #      rsync -av --delete "${src}" "${dst}"
    check_files_owner_compatibility "${src}"
    if ((is_old_rdiff)) ; then
        do_sudo rdiff-backup "${src}" "${dst}"
    else
        do_sudo rdiff-backup $rdiff_opts backup "${src}" "${dst}"
    fi

    version="$( _rdiff_list_timestamps "$dst" | wc -l )"

    if ! ((is_quiet)) ; then
        echo -e "\r\033[K${el_c_g}Saved${el_c_n}  [${version}]"
    fi
    #echo -e "${el_c_g}Saved${el_c_n} $(du -hs ${2} | awk '{print $1}')"

}


do_get(){
    local version history_num
    local src="${1%/}"
    local dst="${2%/}"
    cd -P "$dst"

    if ! [[ -d "$bkpdir_id" ]] ; then
        echo -e "E: dir $bkpdir_id not exists, not backup saved yet ?"
        exit 1
    fi

    if [[ -n "$3" ]] ; then
        history_num="$3"
    else
        history_num="1"
    fi

    if ((is_quiet)) ; then
        echo -ne "Recovering..."
    fi

    version="$( _rdiff_list_timestamps "$src" | tail -${history_num} | head -1 )"

    el_check_variables "version"

    do_sudo rm -rf "${src}.get"

    #check_files_owner_compatibility "${src}" # we don't need to check a dir to restore, so we have checked it at saving
    if ((is_old_rdiff)) ; then
        do_sudo rdiff-backup -r "$version" "${src}" "${src}.get"
    else
        do_sudo rdiff-backup $rdiff_opts restore --at "$version" "${src}" "${src}.get"
    fi

    if ((is_quiet)) ; then
        do_sudo rsync -aq --delete "${src}.get/" "${dst}/"
        echo -e "\r\033[KRecovered      "
    else
        do_sudo rsync -a --delete --info=flist0,name "${src}.get/" "${dst}/"
    fi

    do_sudo rm -rf "${src}.get"

    cd -P "$dst"

}


do_list(){
    if [[ -d "$1" ]] ; then
        if ((is_old_rdiff)) ; then
            do_sudo rdiff-backup -l "${1}"
            if ! ((is_quiet)) ; then
                echo ""
                do_sudo rdiff-backup --parsable-output -l "${1}"
            fi
        else
            do_sudo rdiff-backup $rdiff_opts list increments "${1}"
            if ! ((is_quiet)) ; then
                echo ""
                do_sudo rdiff-backup $rdiff_opts --parsable-output list increments "${1}"
            fi
        fi
    else
        if ! ((is_quiet)) ; then
            el_info "no backups found for this directory"
        fi
    fi
}


do_meld(){
    local version history_num is_done
    local src="${1%/}"
    local dst="${2%/}"

    if ! ((is_done)) && [[ -d "$bkpdir_id" ]] ; then
        if ! el_dependencies_check meld ; then
            if ! el_dependencies_install meld ; then
                el_error "Install required dependencies"
            fi
        fi

        if [[ -n "$3" ]] ; then
            history_num="$3"
        else
            history_num="1"
        fi

        version="$( _rdiff_list_timestamps "$src" | tail -${history_num} | head -1 )"

        el_check_variables "version"

        do_sudo rm -rf "${src}.differ"
        if ((is_old_rdiff)) ; then
            do_sudo rdiff-backup -r "$version" "${src}" "${src}.differ"
        else
            do_sudo rdiff-backup $rdiff_opts restore --at "$version" "${src}" "${src}.differ"
        fi

        echo -e "\n\n${el_c_y}###############################################################${el_c_n}\n"

        do_sudo meld "${src}.differ/" "${dst}/"

        do_sudo rm -rf "${src}.differ"

        is_done=1
    fi

    if ! ((is_done)) && [[ -d "$(readlink -f .).snapshot" ]] ; then
        do_sudo meld "$(readlink -f .).snapshot/" "$(readlink -f .)/"

        is_done=1
    fi


    if ! ((is_done)) ; then
        echo -e "E: dir $bkpdir_id not exists, not backup saved yet ?"
        return 1
    fi

    cd -P "$dst"
}


do_diff(){
    local version history_num
    local src="${1%/}"
    local dst="${2%/}"

    if ! [[ -d "$bkpdir_id" ]] ; then
        echo -e "E: dir $bkpdir_id not exists, not backup saved yet ?"
    fi

    if [[ -n "$3" ]] ; then
        history_num="$3"
    else
        history_num="1"
    fi

    version="$( _rdiff_list_timestamps "$src" | tail -${history_num} | head -1 )"

    el_check_variables "version"

    do_sudo rm -rf "${src}.differ"

    if ((is_old_rdiff)) ; then
        do_sudo rdiff-backup -r "$version" "${src}" "${src}.differ"
    else
        do_sudo rdiff-backup $rdiff_opts restore --at "$version" "${src}" "${src}.differ"
    fi

    #echo -e "\n\n${el_c_y}###############################################################${el_c_n}\n"

    local diff_excludes=(--exclude=".git*" --exclude=".aider*" --exclude=".cecli*")

    #diff -Naur "${diff_excludes[@]}" "${src}.differ/" "${dst}/" | strings | sed -e "s|--- ${src}.differ|--- a|g" -e "s|+++ ${dst}|+++ b|g" | colordiff
    # seems like "strings" eat words, but maybe we need it?
    diff -Naur "${diff_excludes[@]}" "${src}.differ/" "${dst}/" | sed -e "s|--- ${src}.differ|--- a|g" -e "s|+++ ${dst}|+++ b|g" | colordiff

    #if [[ -n "$( which lsdiff )" ]] ; then
        #echo -e "\n ${el_c_y}Total different files:${el_c_n} $( diff -Naur "${diff_excludes[@]}" "${src}.differ/" "${dst}/" | lsdiff | wc -l )"
        #diff -Naur "${diff_excludes[@]}" "${src}.differ/" "${dst}/" | lsdiff | sed 's|^.*rdiff\.differ/||g'
    #else
        local _different_files
        _different_files="$( diff -Naur "${diff_excludes[@]}" "${src}.differ/" "${dst}/" | sed -r -e "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g" -e "s/\x0f//g" | grep '^\-\-\-\s' | wc -l )"
        echo -e "\n ${el_c_y}Total different files:${el_c_n} $_different_files" 1>&2
        diff -Naur "${diff_excludes[@]}" "${src}.differ/" "${dst}/" | sed -r -e "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g" -e "s/\x0f//g" | grep '^\-\-\-\s' | sed -r -e 's|^.*rdiff\.differ/||g' -e 's|([[:digit:]]{4})-([[:digit:]]{2})-([[:digit:]]{2}).*$||g' 1>&2
    #fi

    do_sudo rm -rf "${src}.differ"

    cd -P "$dst"
}


do_diff_reverse(){
    local version history_num
    local src="${1%/}"
    local dst="${2%/}"

    if ! [[ -d "$bkpdir_id" ]] ; then
        echo -e "E: dir $bkpdir_id not exists, not backup saved yet ?"
    fi

    if [[ -n "$3" ]] ; then
        history_num="$3"
    else
        history_num="1"
    fi

    version="$( _rdiff_list_timestamps "$src" | tail -${history_num} | head -1 )"

    el_check_variables "version"

    do_sudo rm -rf "${src}.differ"
    if ((is_old_rdiff)) ; then
        do_sudo rdiff-backup -r "$version" "${src}" "${src}.differ"
    else
        do_sudo rdiff-backup $rdiff_opts restore --at "$version" "${src}" "${src}.differ"
    fi

    local diff_excludes=(--exclude=".git*" --exclude=".aider*" --exclude=".cecli*")

    #echo -e "\n\n${el_c_y}###############################################################${el_c_n}\n"

    diff -Naur "${diff_excludes[@]}" "${dst}/" "${src}.differ/" | strings | sed -e "s|+++ ${src}.differ|+++ a|g" -e "s|--- ${dst}|--- b|g" | colordiff

    #if [[ -n "$( which lsdiff )" ]] ; then
        #echo -e "\n ${el_c_y}Total different files:${el_c_n} $( diff -Naur "${diff_excludes[@]}" "${dst}/" "${src}.differ/" | lsdiff | wc -l )"
        #diff -Naur "${diff_excludes[@]}" "${dst}/" "${src}.differ/" | lsdiff | sed 's|^.*rdiff\.differ/||g'
    #else
        local _different_files
        _different_files="$( diff -Naur "${diff_excludes[@]}" "${dst}/" "${src}.differ/" | sed -r -e "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g" -e "s/\x0f//g" | grep '^\-\-\-\s' | wc -l )"
        echo -e "\n ${el_c_y}Total different files:${el_c_n} $_different_files" 1>&2
        diff -Naur "${diff_excludes[@]}" "${dst}/" "${src}.differ/" | sed -r -e "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g" -e "s/\x0f//g" | grep '^\-\-\-\s' | sed -r -e 's|^.*rdiff\.differ/||g' -e 's|([[:digit:]]{4})-([[:digit:]]{2})-([[:digit:]]{2}).*$||g' 1>&2
    #fi

    do_sudo rm -rf "${src}.differ"

    cd -P "$dst"
}


do_remove(){
    local src="${1%/}"
    local dst="${2%/}"
    cd -P "$dst"

    if [[ -d "${src}" ]] ; then
        do_sudo rm -rf "${src}"
        do_sudo rm -rf "${src}."*

        if ! ((is_quiet)) ; then
            echo -e "${el_c_r}Removed dir ${el_c_y}${src}${el_c_n}"
        fi
    else
        if ! ((is_quiet)) ; then
            echo -e "${el_c_r}E: ${el_c_n}Already removed ?"
        fi
    fi
}



main(){
    # pre {{{
    local tool namedir bkpdir_id target history_num bkp_id

    tool="$(basename $0)"
    # Use the real path to avoid issues with symlinks and ensure consistent IDs
    # We use -P to ensure we are in the physical directory from the start
    cd -P .
    namedir="$( readlink -f "$(pwd)" )"
    namedir="${namedir//\//%}"
    if [[ -n $XDG_CACHE_HOME ]] ; then
        bkpdir_main="${XDG_CACHE_HOME}/${tool}"
    else
        bkpdir_main="${HOME}/.cache/${tool}"
    fi
    bkpdir_backups="${bkpdir_main}/backups"
    bkpdir_works="${bkpdir_main}/working"

    # TODO: delete
    if [[ -d "${HOME}/.${tool}.backups" ]] ; then
        if ! ((is_quiet)) ; then
            el_warning "you still having the old dir for backups, remove it entirely: ${HOME}/.${tool}.backups"
        fi
    fi


    # do we have automated access to sudo ? use sudo
    if el_check_sudo_automated 2>/dev/null ; then
        SUDO_MODE="sudo"
    else
        SUDO_MODE=""
    fi



    if ! el_dependencies_check "rsync|rdiff-backup|diff|colordiff" ; then
        if ! el_dependencies_install "rsync|rdiff-backup|diffutils|colordiff" ; then
            el_error "Install required dependencies"
        fi
    fi

    # }}}

    # Parse arguments first to get bkp_id
    local args_to_process=()
    while (( "$#" )); do
        case "$1" in
            --id|-i)
                bkp_id="${2}"
                shift 2
                ;;
            --quiet|-q)
                is_quiet=1
                shift
                ;;
            *)
                args_to_process+=("$1")
                shift
                ;;
        esac
    done

    # Define ID-dependent paths after parsing arguments
    bkpdir_id="${bkpdir_backups}/${namedir}${bkp_id:+_$bkp_id}.rdiff"
    target="$( readlink -f "$(pwd)" )"

    el_check_variables "tool|namedir|bkpdir_id|target"
    el_check_dirs      "${target}"

    el_debug "Backup directory: ${bkpdir_id}"

    # Process remaining commands
    set -- "${args_to_process[@]}"
    while (( "$#" )); do
        case "$1" in
            work|w|-w)
                do_work
                shift
                ;;
            save|s|-s|--save)
                do_save "${target}" "${bkpdir_id}"
                shift
                ;;
            get|g|-g|--get)
                do_get "${bkpdir_id}" "${target}" "$2"
                [[ -n "$2" ]] && shift
                shift
                ;;
            snap|x|-x|--snap|snapshot|--snapshot)
                do_snap "${target}"
                shift
                ;;
            list|l|-l|--list)
                do_list "${bkpdir_id}"
                shift
                ;;
            diff)
                do_diff "${bkpdir_id}" "${target}" "${2}"
                [[ -n "$2" ]] && shift
                shift
                ;;
            diff-reverse)
                do_diff_reverse "${bkpdir_id}" "${target}" "${2}"
                [[ -n "$2" ]] && shift
                shift
                ;;
            meld)
                do_meld "${bkpdir_id}" "${target}" "${2}"
                [[ -n "$2" ]] && shift
                shift
                ;;
            remove|-r|forget)
                do_remove "${bkpdir_id}" "${target}"
                shift
                ;;
            purge)
                do_purge_olds "${bkpdir_backups}"
                shift
                ;;
            *)
                shift
                ;;
        esac
    done



}

# Usage
if [[ -z "${1}" ]] ; then
    echo -e "Usage: $(basename "$BASH_SOURCE") [options] <command>"
    echo -e ""
    echo -e "Options:"
    echo -e "  --id, -i ID      Specify a unique identifier for the backup (allows multiple backups for one dir)"
    echo -e "  --quiet, -q      Minimize output (useful for scripts)"
    echo -e ""
    echo -e "Commands:"
    echo -e "  save             Create a new incremental backup of the current directory"
    echo -e "  get {n}          Restore the directory to the state of the n-th previous backup (default: 1)"
    echo -e "  list             List all available backup increments and their timestamps"
    echo -e "  diff {n}         Show a colorized diff between current files and the n-th backup"
    echo -e "  diff-reverse {n} Show a diff in reverse (backup vs current)"
    echo -e "  meld {n}         Open 'meld' (GUI) to compare/merge current files with the n-th backup"
    echo -e "  work             Create a temporary sandbox copy of the dir to work in; merges/discards on exit"
    echo -e "  snap             Create a BTRFS-based COW snapshot and drop into a shell; restore or discard on exit"
    echo -e "  remove           Delete the backup history for the current directory"
    echo -e "  purge            Delete ALL backups and working states managed by this tool for all directories"
    echo -e ""
    echo -e "  Note: {n} refers to the increment number (1 is the most recent backup)"
    exit 1
fi



#
#  MAIN
#
main "$@"

# vim: set foldmethod=marker :
