#!/bin/bash

declare -a pkgs
while read pkg; do
    pkgs+=("${pkg}")
done

declare conf_file
conf_file='/etc/pacman-zfs-snapshot.conf'

declare important_names snaps_trivial_keep snaps_important_keep snaps_trivial_suffix snaps_important_suffix
if [[ -r "${conf_file}" ]]; then
    source "${conf_file}"
fi

# User-defined
do_dry_run="${do_dry_run:-false}"
important_names="${important_names:-linux|systemd|zfs-(dkms|utils)}"
snaps_trivial_keep="${snaps_trivial_keep:-15}"
snaps_important_keep="${snaps_important_keep:-5}"
snaps_trivial_suffix="${snaps_trivial_suffix:-trv}"
snaps_important_suffix="${snaps_important_suffix:-imp}"
pkgs_list_max_length="${pkgs_list_max_length:-30}"
snap_only_local_datasets="${snap_only_local_datasets:-true}"
snap_field_separator="${snap_field_separator:-_}"
snap_name_prefix="${snap_name_prefix:-pacman}"
snap_date_format="${snap_date_format:-%F-%H%M}"
snap_op_installation_suffix="${snap_op_installation_suffix:-inst}"
snap_op_remove_suffix="${snap_op_remove_suffix:-rmvl}"
snap_op_upgrade_suffix="${snap_op_upgrade_suffix:-upgr}"

# Internal
declare pkg_separator max_zfs_snapshot_name_length color_reset color_lyellow color_red
pkg_separator=':'
max_zfs_snapshot_name_length='255'
color_reset='\e[0m'
color_lyellow='\e[93m'
color_red='\e[31m'

declare operation conf_op_suffix
operation="${1}"
case "${operation}" in
    install)
        conf_op_suffix="${snap_op_installation_suffix}"
        ;;
    remove)
        conf_op_suffix="${snap_op_remove_suffix}"
        ;;
    upgrade)
        conf_op_suffix="${snap_op_upgrade_suffix}"
        ;;
esac

function pprint () {
    local style msg exit_code
    style="${1:?}"
    msg="${2:?}"
    exit_code="${3}"

    case "${style}" in
        warn)
            printf -- "${color_lyellow}"'[WARN]'"${color_reset}"' %s\n' "${msg}"
            ;;
        err)
            printf -- "${color_red}"'[ERR]'"${color_reset}"' %s\n' "${msg}"
            ;;
        info)
            printf -- '[INFO] %s\n' "${msg}"
            ;;
    esac

    [[ "${exit_code}" ]] && exit "${exit_code}"
}

function split_pkgs_by_importance () {
    local pkgs_in_transaction
    pkgs_in_transaction=("${@}")
    for pkg in "${pkgs_in_transaction[@]}"; do
        if grep -Piq -- '^('"${important_names}"')$' <<<"${pkg}"; then
            important_pkgs_in_transaction+=("${pkg}")
        else
            trivial_pkgs_in_transaction+=("${pkg}")
        fi
    done
}

function set_severity () {
    if [[ "${#important_pkgs_in_transaction[@]}" -ge '1' ]]; then
        severity="${snaps_important_suffix}"
    else
        severity="${snaps_trivial_suffix}"
    fi
}

function get_globally_snappable_datasets () {
    local datasets_list
    # For all datasets show their 'space.quico:auto-snapshot' property; only
    # print dataset name in column 1 and property value in column 2. In awk
    # limit this list to datasets where tab-delimited column 2 has exact
    # string '^true$' then further limit output by eliminating snapshots
    # from list, i.e. dataset names that contain an '@' character.
    datasets_list="$(zfs get -H -o 'name,value' 'space.quico:auto-snapshot' | \
                     awk -F'\t' '{if($2 ~ /^true$/ && $1 !~ /@/) print $1}')"
    while IFS= read -u10 -r dataset; do
        globally_snappable_datasets+=("${dataset}")
    done 10<<<"${datasets_list}"
}

function get_local_snappable_datasets () {
    local datasets_list
    datasets_list="$(findmnt --json --list --output 'fstype,source,target' | \
                     jq --raw-output '.[][] | select(.fstype=="zfs") | .source')"
    while IFS= read -u10 -r dataset; do
        local_snappable_datasets+=("${dataset}")
    done 10<<<"${datasets_list}"
}

function trim_globally_snappable_datasets () {
    for global_dataset in "${globally_snappable_datasets[@]}"; do
        for local_dataset in "${local_snappable_datasets[@]}"; do
            if grep -Piq -- '^'"${local_dataset}"'$' <<<"${global_dataset}"; then
                snappable_datasets+=("${global_dataset}")
            fi
        done
    done
}

function write_pkg_list_oneline () {
    if [[ "${severity}" == 'imp' ]]; then
        for pkg in "${important_pkgs_in_transaction[@]}"; do
            if [[ "${unabridged_pkg_list_oneline}" ]]; then
                unabridged_pkg_list_oneline="${unabridged_pkg_list_oneline}${pkg_separator}${pkg}"
            else
                unabridged_pkg_list_oneline="${pkg}"
            fi
        done
    fi
    if [[ "${#trivial_pkgs_in_transaction[@]}" -ge '1' ]]; then
        for pkg in "${trivial_pkgs_in_transaction[@]}"; do
            if [[ "${unabridged_pkg_list_oneline}" ]]; then
                unabridged_pkg_list_oneline="${unabridged_pkg_list_oneline}${pkg_separator}${pkg}"
            else
                unabridged_pkg_list_oneline="${pkg}"
            fi
        done
    fi
}

function find_max_dataset_name_length () {
    local longest_op_suffix op_suffix_string
    longest_op_suffix='0'
    for op_suffix in "${snap_op_installation_suffix}" "${snap_op_remove_suffix}" "${snap_op_upgrade_suffix}"; do
        if [[ "${#op_suffix}" -gt "${longest_op_suffix}" ]]; then
            longest_op_suffix="${#op_suffix}"
        fi
    done
    op_suffix_string="$(head -c "${longest_op_suffix}" '/dev/zero' | tr '\0' '_')"

    local longest_sev_suffix sev_suffix_string
    longest_sev_suffix='0'
    for sev_suffix in "${snaps_trivial_suffix}" "${snaps_important_suffix}"; do
        if [[ "${#sev_suffix}" -gt "${longest_sev_suffix}" ]]; then
            longest_sev_suffix="${#sev_suffix}"
        fi
    done
    sev_suffix_string="$(head -c "${longest_sev_suffix}" '/dev/zero' | tr '\0' '_')"

    local dataset_name_no_pkgs
    max_dataset_name_length='0'
    for dataset in "${snappable_datasets[@]}"; do
        dataset_name_no_pkgs="${dataset}"'@'"${snap_name_prefix}${snap_field_separator}${date_string}${snap_field_separator}"'op:'"${op_suffix_string}${snap_field_separator}"'sev:'"${sev_suffix_string}${snap_field_separator}"'pkgs:'
        if [[ "${#dataset_name_no_pkgs}" -gt "${max_dataset_name_length}" ]]; then
            max_dataset_name_length="${#dataset_name_no_pkgs}"
        fi
    done

    if [[ "${max_dataset_name_length}" -gt "${max_zfs_snapshot_name_length}" ]]; then
        pprint 'warn' 'Snapshot name would exceed ZFS '"${max_zfs_snapshot_name_length}"' chars limit. Skipping snapshots ...' '0'
    fi
}

function trim_pkg_list_oneline () {
    local available_pkg_list_length
    available_pkg_list_length="$((${max_zfs_snapshot_name_length} - ${max_dataset_name_length}))"
    if [[ "${available_pkg_list_length}" -lt "${pkgs_list_max_length}" ]]; then
        # If we have fewer characters available than the user wants limit
        # package list length
        pkgs_list_max_length="${available_pkg_list_length}"
    fi

    local shorter_pkg_list
    shorter_pkg_list="${unabridged_pkg_list_oneline}"
    while [[ "${#shorter_pkg_list}" -gt "${pkgs_list_max_length}" ]]; do
        shorter_pkg_list="${shorter_pkg_list%${pkg_separator}*}"
        if ! grep -Piq "${pkg_separator}" <<<"${shorter_pkg_list}"; then
            # Only one package remains in package list, no need to continue
            break
        fi
    done
    if [[ "${#shorter_pkg_list}" -gt "${pkgs_list_max_length}" ]]; then
        # If this is still too long we empty the package list
        shorter_pkg_list=''
    fi
    trimmed_pkg_list_oneline="${shorter_pkg_list}"
}

function do_snaps () {
    local snap_name snap_return_code
    for snappable_dataset in "${snappable_datasets[@]}"; do
        snap_name="${snappable_dataset}"'@'"${snap_name_prefix}${snap_field_separator}${date_string}${snap_field_separator}"'op:'"${conf_op_suffix}${snap_field_separator}"'sev:'"${severity}${snap_field_separator}"'pkgs:'"${trimmed_pkg_list_oneline}"
        if [[ "${do_dry_run}" == 'true' ]]; then
            pprint 'info' 'Dry-run, pretending to run zfs snapshot '"${snap_name}"
        else
            zfs snapshot "${snap_name}"
            snap_return_code="${?}"
            if [[ "${snap_return_code}" -eq '0' ]]; then
                successfully_snapped_datasets+=("${snappable_dataset}")
                pprint 'info' 'Snapshot done: '"${snap_name}"
            else
                pprint 'warn' 'Snapshot failed: '"${snap_name}"
            fi
        fi
    done
}

function get_snaps_in_cur_sev () {
    local dataset_to_query
    dataset_to_query="${1:?}"
    snap_list="$(zfs list -H -o 'name' -t snapshot "${dataset_to_query}")"
    snaps_done_by_us="$(grep -Pi -- '@'"${snap_name_prefix}${snap_field_separator}" <<<"${snap_list}")"
    snaps_in_cur_sev="$(grep -Pi -- "${snap_field_separator}"'sev:'"${severity}${snap_field_separator}" <<<"${snaps_done_by_us}")"
    printf -- '%s\n' "${snaps_in_cur_sev}"
}

function do_retention () {
    local snap_list snaps_done_by_us snaps_in_cur_sev snaps_limit oldest_snap snap_return_code
    if [[ "${do_dry_run}" == 'true' ]]; then
        pprint 'info' 'Dry-run, skipping potential zfs destroy operations ...'
    else
        for successfully_snapped_dataset in "${successfully_snapped_datasets[@]}"; do        
            snaps_in_cur_sev="$(get_snaps_in_cur_sev "${successfully_snapped_dataset}")"
            if [[ "${severity}" == "${snaps_important_suffix}" ]]; then
                snaps_limit="${snaps_important_keep}"
            else
                snaps_limit="${snaps_trivial_keep}"
            fi
            while [[ "$(get_snaps_in_cur_sev "${successfully_snapped_dataset}" | wc -l)" -gt "${snaps_limit}" ]]; do
                oldest_snap="$(get_snaps_in_cur_sev "${successfully_snapped_dataset}" | head -n1)"
                zfs destroy "${oldest_snap}"
                snap_return_code="${?}"
                if [[ "${snap_return_code}" -eq '0' ]]; then
                    pprint 'info' 'Oldest in chain '"'"'sev:'"${severity}"''"'"' destroyed: '"${oldest_snap}"
                else
                    pprint 'warn' 'Snapshot destruction failed: '"${oldest_snap}"
                fi
            done
        done
    fi
}

function main () {
    local pkgs_in_transaction
    pkgs_in_transaction=("${@}")

    local -a important_pkgs_in_transaction trivial_pkgs_in_transaction
    split_pkgs_by_importance "${pkgs_in_transaction[@]}"

    local severity
    set_severity

    local -a globally_snappable_datasets
    get_globally_snappable_datasets

    local -a snappable_datasets
    if [[ "${snap_only_local_datasets}" == 'true' ]]; then
        local local_snappable_datasets
        get_local_snappable_datasets
        trim_globally_snappable_datasets
    else
        snappable_datasets=("${globally_snappable_datasets}")
    fi

    local unabridged_pkg_list_oneline
    write_pkg_list_oneline

    local date_string max_dataset_name_length
    date_string="$(date +"${snap_date_format}")"
    find_max_dataset_name_length

    local trimmed_pkg_list_oneline
    trim_pkg_list_oneline

    local -a successfully_snapped_datasets
    do_snaps
    do_retention
}

main "${pkgs[@]}"