#!/bin/bash
# barium helper scripts
# author: rosalinux.ru: betcher_
# getmod is a fork of pfs-utils getpfs

INSTALL_DIR="/.memory/layer-base/1/modules"
[ -d "$INSTALL_DIR" ] || INSTALL_DIR="/.memory/layer-base/0/modules"

SORT_STR='-t = -bk2,2n -bk3,3n -bk4,4n'  # параметры сортировки
DEBUG='off'

# явки пароли
REPOTOKEN=""
export RSYNC_PASSWORD=""
[ -f /etc/dnf/vars/token ] && REPOTOKEN="$(head -n1 /etc/dnf/vars/token)"
[ -f /etc/barium/rsync.key ] && export RSYNC_PASSWORD=$(cat /etc/barium/rsync.key)

# Подключаем библиотеку
if [ -f "$(dirname "$0")/lib" ] ;  then
    . "$(dirname "$0")/lib"
else
    # shellcheck disable=SC2230
    . "$(which lib)" || exit 1
fi

run() {
    local cmd=("$@")

    if [ "$DEBUG" = "on" ]; then
        echo "==> ${cmd[*]}" >&2
    fi

    if [ "$GUIMODE" = "on" -o "$DEBUG" != "on" ]; then
        "${cmd[@]}" 2>/dev/null
    else
        "${cmd[@]}"
    fi

    return $?
}

# $1 modname
get_requires() {
    local deplist
    deplist=$(barium modinfo "$1" 2>/dev/null | grep 'stack.order' | cut -f 4- -d ' ')
    [ -z "$deplist" ] && return
    echo "$deplist" | tr ' ' '\n' | sed -e 's/-\{2,\}//g' -e '/^[[:space:]]*$/d' >> "${WORK_DIR}/requires.lst"
}


check_net() {
    # Проверяем доступность ya.ru через /dev/tcp с таймаутом
    if timeout 3 bash -c ':< /dev/tcp/ya.ru/80' 2>/dev/null; then
        return 0
    fi
    echo "Network seems unavailable. You can establish connection now and continue."
    echo "Abort $(basename "$0")? (Y/y)"
    read -r qqq
    if [ "$qqq" = 'y' ] || [ "$qqq" = 'Y' ]; then
        exit $LINENO
    fi
}

# Функция для вывода списка всех доступных модулей
list_all_modules() {
    # Проверяем наличие реполиста
    if [ ! -f "${WORK_DIR}/sfs-full-repolist" ]; then
        echo "Repository list not found. Run '$(basename "$0") -u' first." >&2
        return 1
    fi

    # Заголовок таблицы
    printf "%-40s %-10s %s\n" "MODULE" "SIZE" "DATE"
    printf "%s\n" "----------------------------------------------------------------"
    awk '{
        # Имя файла
        n = split($1, parts, "/")
        module = parts[n]

        # Размер
        size = "-"
        for (i=2; i<=NF; i++) {
            if (match($i, /^SIZE=/)) {
                size = substr($i, 6)
                break
            }
        }

        # Дата
        date = "-"
        for (i=2; i<=NF; i++) {
            if (match($i, /^DATE=/)) {
                date = substr($i, 6)
                break
            }
        }

        printf "%-40s %-10s %s\n", module, size, date
    }' "${WORK_DIR}/sfs-full-repolist"
}

# Сделать реполист для папки $1
# Если указать имя с путем для файла реполиста ($2), то будет создан реполист без подписи,
# без сжатия и с прописанными путями к каждому файлу. Это нужно для указания папки с модулями в качестве репы.
# Если указан только $1 будет создан реполист со стандартным именем _REPOLIST.gz
# В случае настроенного gpg, также будет создан отдельный файл ключ _REPOLIST.gz.sig
# Каталог с этими файлами можно целиком синкать на сервер с любым протоколом, без правок реполиста.
# $1 папка
# $2 реполист вне репы
mkrepolist() {
    local reponame REPOLIST DATE SIZE VER REV MD5 LOCAL a NAME PACKNAME \
          MODVER MODREV ALIASES PACKALIASES AUTHOR MODAUTHOR TEMP_DIR info_file
    reponame="$(realpath "$1")"
    REPOLIST="${reponame}/_REPOLIST"
    if [ "$2" ] ; then
        REPOLIST="$2"
        LOCAL=yes
    fi

    # Создаем или очищаем файл реполиста
    : > "$REPOLIST" || {
        echo "ERROR: Cannot create repolist file $REPOLIST" >&2
        return 1
    }

    while IFS= read -r -d '' a; do
        # Проверяем доступность файла
        if [ ! -r "$a" ]; then
            echo "WARNING: Cannot read $a, skipping" >&2
            continue
        fi

        if [ "$LOCAL" = yes ] ; then
            SUBPATH_a="file:/$a"
        else
            SUBPATH_a="${a#"${reponame}/"}"
        fi

        # Создаем временную директорию с проверкой
        TEMP_DIR=$(mktemp -d) || {
            echo "ERROR: Cannot create temporary directory" >&2
            continue
        }

        # Извлекаем INFO файл из модуля
        if unsquashfs -d "$TEMP_DIR" "$a" -e "${METAINFODIR}/INFO" >/dev/null 2>&1; then
            info_file="$TEMP_DIR/${METAINFODIR}/INFO"
            if [ -f "$info_file" ]; then
                MODREV=$(grep '^MODREV=' "$info_file" | cut -d'=' -f2- | tr -d '"'"'")
                MODVER=$(grep '^MODVER=' "$info_file" | cut -d'=' -f2- | tr -d '"'"'")
                NAME=$(grep '^NAME=' "$info_file" | cut -d'=' -f2- | tr -d '"'"'")
                AUTHOR=$(grep '^AUTHOR=' "$info_file" | cut -d'=' -f2- | tr -d '"'"'")
            fi
        fi

        # Удаляем временную директорию
        rm -rf "$TEMP_DIR"

        # Получаем информацию о файле
        DATE=$(ls -la "$a" --time-style=+%F | awk '{print $6}')
        SIZE=$(du -h "$a" | cut -f1)
        VER="${MODVER:-0}"
        REV="${MODREV:-0}"
        PACKNAME="${NAME:-$(basename "$a")}"
        PACKALIASES="${ALIAS:-$PACKNAME}"
        MODAUTHOR="${AUTHOR:-unknown}"
        MD5=$(md5sum "$a" | awk '{print $1}')

        # Записываем в реполист
        printf "%s VER=%s REV=%s DATE=%s NAME=%s ALIASES=%s AUTHOR=%s SIZE=%s MD5=%s\n" \
               "$SUBPATH_a" "$VER" "$REV" "$DATE" "$PACKNAME" "$PACKALIASES" \
               "$MODAUTHOR" "$SIZE" "$MD5" >> "$REPOLIST"

        # Сбрасываем переменные
        unset VER REV NAME PACKNAME MODVER MODREV DATE SIZE MD5 ALIASES PACKALIASES AUTHOR MODAUTHOR
    done < <(find "$reponame" -maxdepth 2 -type f -name "*.$EXT" -print0)

    if [ "$LOCAL" != yes ] ; then
        # Сжимаем реполист
        if gzip -c "$REPOLIST" > "${REPOLIST}.gz" 2>/dev/null; then
            rm -f "$REPOLIST"
            REPOLIST="${REPOLIST}.gz"
            # Пытаемся подписать, но не прерываем выполнение при ошибке
            gpg --detach-sign --output "${REPOLIST}.sig" "$REPOLIST" 2>/dev/null
        fi
    fi

    [ -f "$REPOLIST" ] && echo "$REPOLIST"
}

# склеивает списки в форматах txt и txt.gz в один
make_full_repolist() {
    local temp_file
    temp_file=$(mktemp) || {
        echo "ERROR: Cannot create temporary file" >&2
        return 1
    }

    # Обрабатываем все списки
    for a in "${WORK_DIR}/lists/"*.list ; do
        [ -f "$a" ] || continue
        # Пробуем распаковать gz, если не получается - читаем как есть
        if ! zcat "$a" 2>/dev/null; then
            cat "$a"
        fi
    done | sort | uniq | awk '{$1=$1; print}' > "$temp_file"

    # Перемещаем временный файл
    mv "$temp_file" "${WORK_DIR}/sfs-full-repolist"
}

# поиск модуля с самой свежей версией
# $1 шаблон поиска для grep (ищет только отдельное слово)
find_mods() {
    local new found_all
    # Ищем точное совпадение с .xzm
    new=$(grep -wi "${1}.xzm" "${WORK_DIR}/sfs-full-repolist" | sort $SORT_STR | head -n1)
    # Если не нашли, ищем просто шаблон
    [ -z "$new" ] && new=$(grep -wi "${1}" "${WORK_DIR}/sfs-full-repolist" | sort $SORT_STR | head -n1)

    if [ "$2" = "new" ] ; then
        echo "$new"
        return
    elif [ "$2" = "all" ] ; then
        # Все найденные модули, кроме самого нового
        found_all=$(grep -wi "$1" "${WORK_DIR}/sfs-full-repolist" | sort $SORT_STR)
        echo "$found_all" | grep -Fxv "$new" | cut -d " " -f1
    fi
}

# поиск по синонимам
# $1 что ищем
# $2 файл с синонимами
find_alias() {
    local FIND LIST
    # Поиск точного совпадения с .xzm
    FIND=$(grep -m1 -w "${1}.xzm" "$2" | awk '{print $1}')
    # Если не нашли, ищем просто слово
    [ -z "$FIND" ] && FIND=$(grep -m1 -w "$1" "$2" | awk '{print $1}')

    # Поиск с учетом ошибок написания если есть agrep
    if [ -z "$FIND" ] && command -v agrep >/dev/null 2>&1; then
        FIND=$(agrep -i -2 -w "$1" "$2" | head -n1 | awk '{print $1}')
    fi

    [ -n "$FIND" ] || return

    # Убираем подчеркивание из имени
    FIND="${FIND#_}"
    # Получаем список синонимов
    LIST=$(grep "_${FIND}" "$2" | sed 's/^_[^ ]* //')
    echo "${1}($FIND): $LIST"
}

checksign() {
    local a ans
    # Импортируем доверенный ключ
    echo "B9A4244BB5C265B282D9C7DBBBDB708F7357F7AA:6:" | run gpg --import-ownertrust 2>/dev/null

    for a in "${PUBKEYS}"/*.gpg ; do
        [ -f "$a" ] || continue
        if run gpg --quiet --verify --keyring "$a" "$1" "$2" ; then
            echo "passed!"
            return 0
        fi
        echo "failed!"
    done

    echo "Incorrect signature in $(basename "$1")"
    echo "Continue? (y/n)"
    read -r ans
    [ "$ans" != "y" ] && [ "$ans" != "Y" ] && exit 1
}

update_repolists() {
    local file sign_file ans a n=1

    # Проверяем наличие файла со списком зеркал
    if [ ! -f "$mirror_list" ]; then
        echo "ERROR: Mirror list file $mirror_list not found" >&2
        return 1
    fi

    while read -r a; do
        case "$a" in
            ''|\#*) continue ;;
        esac

        file=''
        sign_file=''

        if [ -d "$a" ] ; then
            # Локальная директория
            file="$(mkrepolist "$a" "${WORK_DIR}/lists/${n}.list")"
        elif [ -f "$a" ] ; then
            # Локальный файл
            file="$(run getfile "$a" "${WORK_DIR}/lists/${n}.list")"
        else
            # Удаленный репозиторий
            [ -n "$REPOTOKEN" ] && a=$(echo "$a" | sed 's#^rsync://#rsync://'"${REPOTOKEN}"'@#')
            file="$(run getfile "$a" "${WORK_DIR}/lists/${n}.list")"
            sign_file="$(run getfile "${a}.sig" "${WORK_DIR}/lists/${n}.list.sig")"

            if ! [ -f "$file" ] ; then
                echo -e "Cannot download repository list\n $a" >&2
                continue
            fi
            if [ -f "$sign_file" ] ; then
                echo -n "Check sign: $file - "
                sleep 0.5
                checksign "$sign_file" "$file"
            else
                echo "Repository list was not signed, or signature was not found"
                echo "Continue? (y/n)"
                read -r ans
                [ "$ans" != "y" ] && [ "$ans" != "Y" ] && exit 1
            fi
            add_repo_path "$file" "$a"
        fi
        n=$((n + 1))
    done < "$mirror_list"
}

# добавляем пути до репы в реполист
# $1 локальный реполист
# $2 реполист с путем до сервера
add_repo_path() {
    local path tmpfile
    path="$(dirname "$2")"
    tmpfile=$(mktemp) || {
        echo "ERROR: Cannot create temporary file" >&2
        return 1
    }

    # Распаковываем или читаем реполист
    if ! zcat "$1" > "$tmpfile" 2>/dev/null; then
        cat "$1" > "$tmpfile"
    fi

    # Добавляем путь к каждому файлу, если его нет
    sed -r '/^[a-z]{3,6}:\/\//!s#^#'"$path"'/#' "$tmpfile" > "$1"
    rm -f "$tmpfile"
}

HLP() {
    cat << EOF
Usage:
    $(basename "$0") mod_name     - find and download module
    $(basename "$0") -m dir       - create repo lists for local dir
    $(basename "$0") -u           - update repo lists from server

Keys:
    -u | --update-media     - update repolists
    -f | --force            - do not ask something
    -v | --verbose          - show commands being executed
    -s | --search           - search only
    -g | --guimode          - formatting the output to use in wrappers
    -m | --mkrepolist       - make repolist for directory with modules
    -h | --help             - this help
    -b | --build            - build the module by chroot2mod, when downloading a script mod.c2m instead mod.$EXT
    -o | --outdir           - set the directory for module
    -i | --install          - same as -o -b
EOF
    exit 1
}

#################### Начало ##############################
separ=''
sourcelist=''
UPDATE_M=""
FORCE=""
SEARCH=""
GUIMODE=""
MKREPOLIST=""
BUILD=""
OUTDIR=""

while [ $# -gt 0 ]; do
    case "$1" in
        -u|--update-media) UPDATE_M="on" ;;
        -f|--force) FORCE="on" ;;
        -s|--search) SEARCH="on" ;;
        -g|--guimode) GUIMODE="on" ;;
        -m|--mkrepolist) MKREPOLIST="on" ;;
        -h|--help) HLP ;;
        -b|--build) BUILD="on" ;;
        -i|--install) OUTDIR="$INSTALL_DIR"; BUILD="on" ;;
        -l|--list) LIST_MODE="on" ;;
        -v|--verbose) DEBUG="on" ;;
        -o|--outdir)
            shift
            OUTDIR="$1"
            ;;
        --) shift; break ;;
        -*)
            echo "$(basename "$0"): invalid option -- '${1#-}'" >&2
            HLP
            ;;
        *) sourcelist="${sourcelist} ${1}" ;;
    esac
    shift
done

sourcelist="${sourcelist# }"

# если --mkrepolist создаем _REPOLIST для указанной папки в ней же
if [ "$MKREPOLIST" = "on" ] ; then
    if [ -z "$sourcelist" ]; then
        echo "ERROR: Directory not specified for mkrepolist" >&2
        exit 1
    fi
    mkrepolist $sourcelist
    exit
fi

# Проверяем сеть, если не в режиме поиска
[ "$SEARCH" != "on" ] && check_net

# Создаем рабочие директории
mkdir -p "${WORK_DIR}/lists" || {
    echo "ERROR: Cannot create ${WORK_DIR}/lists" >&2
    exit 1
}

# если ключ -u или холодный старт создаем списки
if [ "$UPDATE_M" = "on" ] || [ ! -f "${WORK_DIR}/sfs-full-repolist" ] ; then
    [ "$GUIMODE" != "on" ] && [ "$DEBUG" == "on" ] && \
    echo '#########################################################################'
    # Очищаем старые списки
    rm -f "${WORK_DIR}/lists/"*.list "${WORK_DIR}/lists/"*.list.sig
    update_repolists
    make_full_repolist
        [ "$GUIMODE" != "on" ] && [ "$DEBUG" == "on" ] && \
    echo '#########################################################################'
fi

if [ "$LIST_MODE" = "on" ]; then
    list_all_modules
    exit
fi

[ -z "$sourcelist" ] && exit

# Обнуляем списки загрузки
: > "${WORK_DIR}/download.lst"
: > "${WORK_DIR}/requires.lst"

# Обрабатываем каждый запрос
for reg in $sourcelist; do
    unset MODPATH MODULE MODULES MAYBE MD5 OVL_FOUND

    # Проверяем, не подключен ли уже такой модуль
    OVL_FOUND=$(barium ls --raw '$bname_source' | grep -w "$reg")

    if [ -f "$reg" ] ; then
        MODULE="$(basename "$reg")"
        TEMP_INFO=$(mktemp -d) || {
            echo "ERROR: Cannot create temp directory" >&2
            continue
        }
        if unsquashfs -d "$TEMP_INFO" "$reg" -e "${METAINFODIR}/INFO" >/dev/null 2>&1; then
            [ -f "$TEMP_INFO/${METAINFODIR}/INFO" ] && reg=$(grep '^NAME=' "$TEMP_INFO/${METAINFODIR}/INFO" | sed 's/^NAME=//')
        fi
        rm -rf "$TEMP_INFO"
    fi

    # Ищем модуль в репозитории
    [ ! -f "$reg" ] && MODULE=$(find_mods "$reg" new)
    [ ! -f "$reg" ] && MODULES=$(find_mods "$reg" all)
    [ -z "$MODULE" ] && MAYBE=$(find_alias "$reg" "$ALIASCFG")

    MODPATH="$(echo "$MODULE" | cut -d " " -f1)"

    # Извлекаем MD5 если есть
    if echo "$MODULE" | grep -q 'MD5='; then
        MD5=$(echo "$MODULE" | sed 's/.*MD5=//' | cut -f1)
    fi

    if [ "$FORCE" = "on" ] ; then
        if [ -z "$MODULE" ] ; then
            echo "Module not found, change search pattern and try again" >&2
            exit 1
        fi
        echo "$MODPATH" "$MD5" >> "${WORK_DIR}/download.lst"
    elif [ "$GUIMODE" = "on" ] ; then
        echo "maybe:>> $MAYBE"
        echo "new:>> $MODPATH"
        for b in $MODULES ; do echo "old:>> $b" ; done
    else
        if [ -n "$OVL_FOUND" ] ; then
            echo "Module matching pattern $reg - $OVL_FOUND is currently loaded"
            echo "Continue? (y/n)"
            read -r qqq
            [ "$qqq" = 'y' ] || [ "$qqq" = 'Y' ] || exit $LINENO
        fi

        if [ -z "$MODULE" ] ; then
            if [ -z "$MAYBE" ] ; then
                echo "$reg not found in repository. Change search pattern and try again" >&2
            else
                echo "Maybe you meant:"
                echo "==> $MAYBE"
                echo "Change search pattern and try again" >&2
            fi
        else
            if [ "$(echo "$MODULES" | wc -w)" -ge 1 ] ; then
                echo -e "\nModules matching pattern \"$reg\":"
                for b in $MODULES ; do echo "==> $b" ; done
            fi
            echo ''
            echo -e "\nProbably you meant:"
            echo -n "==> "
            for item in $MODULE; do
                echo "$item"
                [ "$DEBUG" != 'on' ] && break
            done
            [ "$SEARCH" = "on" ] && continue

            echo "Download? (Y/N)"
            read -r d
            if [ "$d" != Y ] && [ "$d" != y ] ; then
                echo "Refine search pattern and try again" >&2
                continue
            fi
        fi
    fi

    [ -n "$MODPATH" ] && echo "$MODPATH" "$MD5" >> "${WORK_DIR}/download.lst"
done

[ "$SEARCH" = "on" ] && exit

# Определяем целевую директорию
TARGET_PATH="${OUTDIR:-./}"
if ! [ -w "$TARGET_PATH" ] ; then
    echo "ERROR: '$TARGET_PATH' is not writable" >&2
    exit ${LINENO}
fi

# Загружаем модули
while read -r a; do
    [ -z "$a" ] && continue

    MOD='' ; MD5='' ; CHECKMD5='no' ; DMOD=''
    DMOD=$(echo "$a" | awk '{print $1}')
    MD5=$(echo "$a" | awk '{print $2}')
    MOD=$(getfile "$DMOD" "$TARGET_PATH/$(basename "$DMOD")")

    if [ -n "$MD5" ] ; then
        if [ -f "$MOD" ]; then
            if md5sum "$MOD" | grep -q "$MD5"; then
                CHECKMD5='ok'
            else
                CHECKMD5='error'
            fi
        fi
    fi

    if [ -f "$MOD" ] && [ "$CHECKMD5" != "error" ] ; then
        [ "$GUIMODE" != "on" ] && echo "==> $(basename "$MOD") - downloaded successfully. (md5 check: $CHECKMD5)"
        [ "$GUIMODE" = "on" ] && echo "download:>> $MOD"
    else
        [ "$GUIMODE" != "on" ] && echo "==> $(basename "$MOD") - download error!!! (md5 check: $CHECKMD5)"
    fi

    # Если это скрипт сборки и запрошена сборка
    if [ "$BUILD" = "on" ] && [ "${MOD: -4}" = '.c2m' ] ; then
        SCRIPT="$MOD"
        MOD="${MOD%.c2m}.$EXT"
        echo "run barium chroot2mod -o '$MOD' --script '$SCRIPT'"
        echo "Press Enter to continue or Ctrl+C to abort"
        read -r qqq
        run barium chroot2mod -o "$MOD" --script "$SCRIPT"
        if [ ! -f "$MOD" ]; then
            echo "ERROR: Failed to build module" >&2
            exit 1
        fi
    fi

    get_requires "$MOD"
done < "${WORK_DIR}/download.lst"


# Обрабатываем зависимости
if [ -s "${WORK_DIR}/requires.lst" ] ; then
    reqlist=''
    allreqlist=$(cat "${WORK_DIR}/requires.lst"  |sort -u)
    if [ -n "$allreqlist" ] ; then
        declare -A processed
        deps_message=''
        while read -r req ; do
            [ -z "$req" ] && continue
            [ "${processed[$req]}" ] && continue
            processed[$req]=1
            # Проверка на самозависимость
            echo "$MOD" |grep -qw "$req" && continue

            deps_message+="  Dependency check: $req - "

            # Проверяем, не подключен ли модуль
            if barium ls --raw '$bname_source' | grep -wq "$req" ; then
                [ "$GUIMODE" != "on" ] && deps_message+="already in rootfs\n"
                continue
            fi

            # Проверяем, не загружен ли модуль
            if find "$TARGET_PATH" -maxdepth 1 -type f -name "*${req}*" | grep -wq "$req" ; then
                [ "$GUIMODE" != "on" ] && deps_message+="already in dir: $TARGET_PATH\n"
                continue
            fi
            deps_message+="not found\n"
            reqlist="$reqlist $req"
        done < "${WORK_DIR}/requires.lst"
    fi
    [ "$DEBUG" == 'on' ] && echo -e "$deps_message"
    M='-f'
    [ "$GUIMODE" = "on" ] && M='-g'
    if [ -n "$reqlist" ] ; then
        [ "$GUIMODE" != "on" ] && [ "$FORCE" != "on"] && {
        echo "Missing dependencies: $reqlist"
        echo "Downloading?? (y/n)"
        read -r qqq
        [ "$qqq" = 'y' ] || [ "$qqq" = 'Y' ] || exit $LINENO
        }
        # Рекурсивно загружаем зависимости
        run $0 $M -o "$TARGET_PATH" $reqlist
    fi
fi
