#! /bin/bash
# vim: shiftwidth=4 tabstop=4 expandtab

get_isos_from_hyp() {
    local hyp="$1"
    local calc_md5="${2:-0}"

    local remote_cmd='
        dir="'"$ISO_DIRECTORY_PATH"'"
        calc_md5="'"$calc_md5"'"
        hyp="'"$hyp"'"

        [[ -d "$dir" ]] || exit 0

        for iso in "$dir"/*; do
            [[ -f "$iso" ]] || continue

            name=$(basename "$iso")
            ((size=$(stat -c "%s" "$iso")/1024))

            if [[ "$calc_md5" -eq 1 ]]; then
                md5=$(md5sum "$iso" | cut -d" " -f1)
                jo hyp="$hyp" name="$name" size="$size" md5="$md5"
            else
                jo hyp="$hyp" name="$name" size="$size"
            fi
        done
    '

    run_on_hyp "$hyp" bash -c "$remote_cmd" 2>/dev/null
}

get_hyps_isos() {
    local md5=0
    local target_hyps=()

    # Parse parameters
    for arg in "$@"; do
        if [[ "$arg" == "--md5" ]]; then
            md5=1
        else
            target_hyps+=("$arg")
        fi
    done

    # Fallback to $HYPS if no hypervisors passed in arguments
    if [[ ${#target_hyps[@]} -eq 0 ]]; then
        # Use read to split $HYPS into array using spaces/tabs
        read -r -a target_hyps <<< "$HYPS"
    fi

    for hyp in "${target_hyps[@]}"; do
        get_isos_from_hyp "$hyp" "$md5"
    done | jq -s '
        group_by(.name) | map(
            . as $group |

            # Find the most frequent {size, md5} signature (mode)
            # In case of a tie, group_by preserves the original order of appearance
            (
                $group
                | group_by({size: .size, md5: .md5})
                | sort_by(-length)[0][0]
            ) as $majority |

            ($majority.size) as $ref_size |
            ($majority.md5) as $ref_md5 |

            {
                name: $group[0].name,
                size: $ref_size,
                hyps: [ $group[] | select(.size == $ref_size and (.md5 == $ref_md5 or .md5 == null)) | .hyp ],
                mismatch_hyps: [ $group[] | select(.size != $ref_size or (.md5 != null and .md5 != $ref_md5)) | .hyp ]
            } + (
                if $ref_md5 then {md5: $ref_md5} else {} end
            )
        )'
}

# Auto-complete twik: allow sourcing this file from complete command to get supported args
[[ ${IN_AUTOCOMPLETER:-0} -eq 1 ]] && return


subcommand__help() {
    if [[ -z "$2" ]] || [[ "$2" == "ls" ]]; then
        echo "$COMMAND ls [-J] [hyp(s)]"
        if [[ "$2" == "ls" ]]; then
            echo "  List CD-ROM ISO files present on hypervisors"
            echo "    -J|--json          JSON output"
            echo "    hyp(s)             Optional list of hypervisors to handle"
        fi
    fi
    if [[ -z "$2" ]] || [[ "$2" == "sync" ]]; then
        echo "$COMMAND sync [from_hyp] [iso(s)]"
        if [[ "$2" == "sync" ]]; then
            echo "  List CD-ROM ISO files present on hypervisors"
            echo "    from_hyp           Synchronize ISO from specified hypervisor (optional, default: master)"
            echo "    iso(s)             Optional list of ISO file to synchronize"
        fi
    fi
}

subcommand__ls() {
    check_generate_script_not_supported
    local isos json=0 md5=0 opt
    local -a hyps args
    for opt in "${@:1}"; do
        case $opt in
            -J|--json)
                json=1
                ;;
            --md5)
                md5=1
                ;;
            *)
                if check_hyp "$opt"; then
                    hyps+=( "$opt" )
                else
                    usage "Unknown parameter '$opt'"
                fi
                ;;
        esac
    done

    [[ ${#hyps[@]} -eq 0 ]] && read -r -a hyps <<< "$HYPS"

    if [[ "$md5" -eq 1 ]]; then
        isos=$( get_hyps_isos --md5 "${hyps[@]}" )
    else
        isos=$( get_hyps_isos "${hyps[@]}" )
    fi

    if [[ $json -eq 1 ]]; then
        jq <<< "$isos"
        exit 0
    fi

    args=( -n Name Size )
    [[ $md5 -eq 1 ]] && args+=( MD5 )
    for hyp in "${hyps[@]}"; do
        args+=( "$hyp" -a "$hyp" c )
    done



    jq --argjson hyps "$( jo -a "${hyps[@]}" )" "$JQ_FORMAT_SIZE"'
        ( map(.size) | add ) as $total_size |
        . as $isos |
        map(
            . as $iso |

            (
                $hyps | map(
                    . as $h |
                    {
                    key: $h,
                    value: (
                        if ($iso.mismatch_hyps // [] | index($h)) then "!"
                        elif ($iso.hyps // [] | index($h)) then "X"
                        else ""
                        end
                    )
                    }
                ) | from_entries
            ) as $matrix |

            {
                Name: .name,
                Size: .size | format_size
            }
            + (if .md5 then {MD5: .md5} else {} end)
            + $matrix
        ) + [
            {
                Name: "Total:",
                Size: $total_size | format_size,
            }
            + (if .[0].md5 then {MD5: ""} else {} end)
            + (
                $hyps |
                map(
                    . as $h |
                    {
                        key: $h,
                        value: (
                            $isos
                            | map(select(
                                ((.hyps // []) + (.mismatch_hyps // [])) | index($h)
                            ))
                            | length
                        )
                    }
                ) | from_entries
            )
        ]
    ' <<< "$isos"  | print_json_table "${args[@]}"

    echo
    echo "Legend:"
    echo " [X] present on the server"
    if [[ "$md5" -eq 1 ]]; then
        echo " [!] present on the server but its size (or MD5) does not matched with file on other host(s)."
    else
        echo " [!] present on the server but its size does not matched with file on other host(s)."
    fi
}

subcommand__sync() {
    check_generate_script_not_supported
    local opt from_hyp master iso iso_path error=0
    local -a isos cmd
    for opt in "${@:1}"; do
        case $opt in
            *)
                if [[ "${from_hyp:-null}" == "null" ]] && check_hyp "$opt"; then
                    from_hyp=$opt
                else
                    check_iso_name "$opt" || usage "Invalid ISO filename '$opt'"
                    isos+=( "$opt" )
                fi
                ;;
        esac
    done

    # If source hypervisor not specified, identified master host
    if [[ "${from_hyp:-null}" == "null" ]]; then
        if is_master; then
            master=$( hostname )
        else
            master=$( run_on_hyp "$VIP" hostname )
        fi
    fi

    # Ask user confirmation
    if [[ "${#isos[@]}" -eq 0 ]]; then
        confirm "Sync ISO directory ($ISO_DIRECTORY_PATH) from ${from_hyp:-master ($master)} to other hypervisors" || exit 1
    else
        confirm "Sync ISO $(implode ', ' "${isos[@]}") from ${from_hyp:-master ($master)} to other hypervisors" || exit 1
    fi

    for hyp in $HYPS; do
        [[ "${from_hyp:-$master}" == "$hyp" ]] && continue
        if [[ "${#isos[@]}" -eq 0 ]]; then
            cmd=( rsync -a "$ISO_DIRECTORY_PATH/" "$hyp:$ISO_DIRECTORY_PATH/" )
            if [[ "${from_hyp:-null}" == "null" ]] && is_master; then
                if run_external "${cmd[@]}"; then
                    display "ISO directory ($ISO_DIRECTORY_PATH) synchronized from master ($master) to $hyp"
                else
                    error "Failed to synchronize ISO directory ($ISO_DIRECTORY_PATH) from master ($master) to $hyp"
                    error=1
                fi
            elif run_on_hyp "${from_hyp:-$VIP}" "${cmd[@]}"; then
                display "ISO directory ($ISO_DIRECTORY_PATH) synchronized from ${from_hyp:-master ($master)} to $hyp"
            else
                error "Failed to synchronize ISO directory ($ISO_DIRECTORY_PATH) from ${from_hyp:-master ($master)} to $hyp"
                error=1
            fi
        else
            for iso in "${isos[@]}"; do
                iso_path="$ISO_DIRECTORY_PATH/$iso"
                # Ensure specified ISO exist
                if [[ "${from_hyp:-null}" == "null" ]] && is_master; then
                    if [[ ! -e "$iso_path" ]]; then
                        error "$iso not found on master ($master)"
                        error=1
                        continue
                    fi
                elif ! run_on_hyp "${from_hyp:-$VIP}" test -e "$iso_path"; then
                    error "$iso not found on ${from_hyp:-master ($master)}"
                    error=1
                    continue
                fi

                # Synchronize ISO file
                cmd=( rsync -a "$iso_path" "$hyp:$iso_path" )
                if [[ "${from_hyp:-null}" == "null" ]] && is_master; then
                    if run_external "${cmd[@]}"; then
                        display "$iso synchronized from master ($master) to $hyp"
                    else
                        error "Failed to synchronize $iso from master ($master) to $hyp"
                        error=1
                    fi
                elif run_on_hyp "${from_hyp:-$VIP}" "${cmd[@]}"; then
                    display "$iso synchronized from ${from_hyp:-master ($master)} to $hyp"
                else
                    error "Failed to synchronize $iso from ${from_hyp:-master ($master)} to $hyp"
                    error=1
                fi
            done
        fi
    done
    return $error
}


run_subcommand "${COMMAND_ARGS[@]}"
