#!/usr/bin/env bash
#
#    dl-distro - Bash script for downloading and verifying OS images.
#
#    Copyright (C) 2023-2025 bashuser30 <bashuser30@mailbox.org>
#
#    dl-distro is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    dl-distro is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program. If not, see <https://www.gnu.org/licenses/>.
#

set -euo pipefail

version="v2.4.9"
data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/dl-distro"
conf_dir="${XDG_CONFIG_HOME:-$HOME/.config}/dl-distro"
json_dir="$data_dir/json"
gpg_dir="$data_dir/gpg"
minisign_dir="$data_dir/minisign"
signify_dir="$data_dir/signify"
json_file="$json_dir/data.json"
user_json_file="$conf_dir/data.json"
conf_file="$conf_dir/dl-distro.conf"
default_download_dir="$PWD"
auto_update_enabled="true"
verification_enabled="true"
spider_mode_enabled="false"
manual_download_enabled="false"
gpg_keyserver="hkps://keyserver.ubuntu.com"
json_url="https://codeberg.org/bashuser30/dl-distro/raw/branch/master/data.json"
green=$'\033[32m'
red=$'\033[31m'
yellow=$'\033[33m'
bold=$'\033[1m'
und=$'\033[4m'
nc=$'\033[39m'
reset=$'\033[0m'
PS3="${bold}>${reset} "

# shellcheck source=/dev/null
[[ -f "$conf_file" ]] && source "$conf_file"
[[ -f "$user_json_file" ]] && json_file="$user_json_file"

declare -A supported_distros=(
	["AlmaLinux OS"]="alma"
	["Alpine"]="alpine"
	["Arch"]="arch"
	["CachyOS"]="cachy"
	["Debian"]="debian"
	["deepin"]="deepin"
	["Dragora"]="dragora"
	["EndeavourOS"]="endeavour"
	["Fedora"]="fedora"
	["GhostBSD"]="ghostbsd"
	["GParted Live"]="gparted"
	["Guix System"]="guix"
	["Hyperbola"]="hyperbola"
	["Kali"]="kali"
	["Mint"]="mint"
	["MX Linux"]="mxlinux"
	["NixOS"]="nix"
	["Nobara"]="nobara"
	["OpenBSD"]="openbsd"
	["openSUSE"]="opensuse"
	["Parabola"]="parabola"
	["Parch"]="parch"
	["Parrot OS"]="parrot"
	["Pop!_OS"]="pop"
	["Proxmox"]="proxmox"
	["PureOS"]="pure"
	["Qubes OS"]="qubes"
	["Rocky"]="rocky"
	["Slackware"]="slackware"
	["Solus"]="solus"
	["Tails"]="tails"
	["Tiny Core"]="tinycore"
	["Trisquel"]="trisquel"
	["Ubuntu"]="ubuntu"
	["Void"]="void"
	["Whonix"]="whonix"
	["Zorin OS"]="zorin"
)

msg()
{
	printf "%b%s%b\n" "$bold" "$1" "$reset"
}

warn()
{
	printf >&2 "%b%bWARNING:%b %s%b\n" "$bold" "$yellow" "$nc" "$1" "$reset"
}

error()
{
	printf >&2 "%b%bERROR:%b %s%b\n" "$bold" "$red" "$nc" "$1" "$reset"
}

die()
{
	error "$1"
	exit 1
}

dependency_check()
{
	missing_deps=()

	for cmd in "$@"; do
		command -v "$cmd" &>/dev/null || missing_deps+=("$cmd")
	done

	if [[ "${#missing_deps[@]}" -gt 0 ]]; then
		die "Please install: ${red}${missing_deps[*]}"
	fi
}

wget_file()
{
	local url

	for url in "$@"; do
		if ! wget -qc --show-progress "$url"; then
			die "Failed to download: ${red}$url"
		fi
	done
}

wget_spider()
{
	[[ "$spider_mode_enabled" == "false" ]] && return 0

	local url

	for url in "$@"; do
		[[ "$(basename "$url")" == "null" ]] && continue

		if ! wget -nvc --spider "$url"; then
			error "Spider failed for: ${red}$url"
		fi
	done

	exit 0
}

fetch_gpg_key()
{
	[[ "$verification_enabled" == "false" ]] && return 0

	dependency_check "gpg"

	gpg_key="$(jq -r ".gpg_keys.$1" "$json_file")"

	if ! gpg --homedir "$gpg_dir" --list-keys "$gpg_key" &>/dev/null; then
		msg "Fetching GPG key..."

		if ! gpg --homedir "$gpg_dir" --keyserver "$gpg_keyserver" --recv-keys "$gpg_key" &>/dev/null; then
			die "Failed to fetch GPG key: ${red}$gpg_key"
		fi
	fi
}

verify_gpg_signature()
{
	[[ "$verification_enabled" == "false" ]] && return 0

	if gpg --homedir "$gpg_dir" --verify "$@" &>/dev/null; then
		msg "GPG verification: ${green}SUCCESS"
	else
		rm -f "$iso_file"
		die "GPG verification: ${red}FAILED"
	fi
}

fetch_minisign_key()
{
	[[ "$verification_enabled" == "false" ]] && return 0

	dependency_check "minisign"

	minisign_key_url="$(jq -r ".minisign_keys.$distro" "$json_file")"
	minisign_key="$(basename "$minisign_key_url")"

	if [[ ! -f "$minisign_dir/$minisign_key" ]]; then
		msg "Fetching Minisign key..."

		if ! wget -P "$minisign_dir" -q "$minisign_key_url"; then
			die "Failed to fetch Minisign key: ${red}$minisign_key_url"
		fi
	fi
}

verify_minisign_signature()
{
	[[ "$verification_enabled" == "false" ]] && return 0

	if minisign -Vp "$minisign_dir/$minisign_key" -x "$sig_file" -m "$sum_file" &>/dev/null; then
		msg "Minisign verification: ${green}SUCCESS"
	else
		die "Minisign verification: ${red}FAILED"
	fi
}

get_signify_name()
{
	local signify_name

	if apt list signify-openbsd 2>/dev/null | grep -q "signify-openbsd"; then
		signify_name="signify-openbsd"
	else
		signify_name="signify"
	fi

	printf "%s" "$signify_name"
}

fetch_signify_key()
{
	[[ "$verification_enabled" == "false" ]] && return 0

	signify_name="$(get_signify_name)"

	dependency_check "$signify_name"

	signify_key_url="$(jq -r ".signify_keys.$distro" "$json_file")"
	signify_key="$(basename "$signify_key_url")"

	if [[ ! -f "$signify_dir/$signify_key" ]]; then
		msg "Fetching Signify key..."

		if ! wget -P "$signify_dir" -q "$signify_key_url"; then
			die "Failed to fetch Signify key: ${red}$signify_key_url"
		fi
	fi
}

verify_signify_signature()
{
	[[ "$verification_enabled" == "false" ]] && return 0

	if "$signify_name" -Vp "$signify_dir/$signify_key" -x "$sig_file" -m "$sum_file" &>/dev/null; then
		msg "Signify verification: ${green}SUCCESS"
	else
		die "Signify verification: ${red}FAILED"
	fi
}

verify_checksum()
{
	if "$sum_algo" -c --ignore-missing "$sum_file" &>/dev/null; then
		msg "$sum_algo verification: ${green}SUCCESS"
	else
		rm -f "$iso_file"
		die "$sum_algo verification: ${red}FAILED"
	fi
}

manual_download()
{
	[[ "$manual_download_enabled" != "true" ]] && return 0

	printf "\e[2J\e[H"

	msg "${green}Download files$reset"
	msg ""

	msg "Image:     $url/$iso_file"
	[[ "$sum_file" != "null" ]] && msg "Checksum:  $url/$sum_file"
	[[ "$sig_file" != "null" ]] && msg "Signature: $url/$sig_file"

	[[ "$sum_file" != "null" ]] && {
		msg ""
		msg "${green}Validate Checksum$reset"
		msg ""
		msg "Navigate to the download directory and run:"
		msg ""
		msg "    $sum_algo -c --ignore-missing $sum_file"
		msg ""
		msg "${yellow}If you see \"FAILED\", the file may be corrupt or tampered with!$reset"
	}

	[[ "$sig_file" != "null" && "$distro" != "void" && "$distro" != "openbsd" ]] && {
		msg ""
		msg "${green}Validate Signature$reset"
		msg ""
		msg "Import the GPG key and validate the signature:"
		msg ""
		msg "    gpg --keyserver $gpg_keyserver --recv-keys $(jq -r ".gpg_keys.$distro" "$json_file")"
		msg "    gpg --verify $sig_file"
		msg ""
		msg "${yellow}If you see \"BAD signature\", the file may be corrupt or tampered with!$reset"
		msg ""
		msg "Now you can delete the GPG key: "
		msg ""
		msg "gpg --yes --batch --delete-keys $(jq -r ".gpg_keys.$distro" "$json_file")"
		exit 0
	}

	[[ "$distro" == "void" ]] && {
		msg ""
		msg "${green}Validate Signature$reset"
		msg ""

		minisign_key_url="$(jq -r ".minisign_keys.$distro" "$json_file")"
		minisign_key="$(basename "$minisign_key_url")"

		msg "Download the Minisign key:"
		msg ""
		msg "    $minisign_key_url"
		msg ""
		msg "Validate the signature:"
		msg ""
		msg "    minisign -Vp $minisign_key -x $sig_file -m $sum_file"
		msg ""
		msg "${yellow}If you see \"Signature verification failed\", the file may be corrupt or tampered with!$reset"
		exit 0
	}

	[[ "$distro" == "openbsd" ]] && {
		msg ""
		msg "${green}Validate Signature$reset"
		msg ""

		signify_key_url="$(jq -r ".signify_keys.$distro" "$json_file")"
		signify_key="$(basename "$signify_key_url")"
		signify_name="$(get_signify_name)"

		msg "Download the Signify key:"
		msg ""
		msg "    $signify_key_url"
		msg ""
		msg "Validate the signature:"
		msg ""
		msg "    $signify_name -Vp $signify_key -x $sig_file -m $sum_file"
		msg ""
		msg "${yellow}If you see \"Signature verification failed\", the file may be corrupt or tampered with!$reset"
		exit 0
	}

	exit 0
}

download_alma()
{

	[[ "$traverse_path" =~ ${distro}_([0-9]+) ]] && distro="${distro}_${BASH_REMATCH[1]}"

	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"

	if [[ "$traverse_path" == *"live"* ]]; then
		wget_file "$url"/{"$sig_file","$sum_file"}
		verify_gpg_signature "$sig_file" "$sum_file"
	else
		wget_file "$url/$sig_file"
		verify_gpg_signature "$sig_file"
	fi

	wget_file "$url/$iso_file"
	verify_checksum
}

download_alpine()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	verify_checksum
}

download_arch()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	verify_checksum
}

download_cachy()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	verify_checksum
}

download_debian()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}

	[[ "$traverse_path" == *"unstable"*"direct"* ]] && {
		wget_file "$url/$iso_file"
		msg "If the upstream process of making latest images has failed, you can't download those!"
		msg "These images are development snapshots and don't have stability."
		msg "Debian does not provide checksum or signature files for this image."
		return 0
	}

	[[ "$traverse_path" == *"unstable"*"indirect"* ]] && {
		msg "If the upstream process of making latest images has failed, you can't download those!"
		msg "These images are development snapshots and don't have stability."
		msg "It is possible to create ISO files, but not to create checksum or signature files!"

		fetch_gpg_key "$distro"
		wget_file "$url"/{"$sig_file","$sum_file"}
		verify_gpg_signature "$sig_file" "$sum_file"
		wget_file "$url/$iso_file"
		verify_checksum
		return 0
	}

	[[ "$traverse_path" == *"ports.gnu_hurd.latest"* || "$traverse_path" == *"ports.gnu_hurd.stable"*"netinst"* ]] && {
		wget_file "$url/$iso_file"
		msg "Debian does not provide checksum or signature files for this image."
		return 0
	}

	[[ "$traverse_path" == *"ports.gnu_hurd"* ]] && {
		wget_file "$url"/{"$sum_file","$iso_file"}
		verify_checksum
		msg "Debian does not provide signature files for this image."
		return 0
	}

	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_gpg_signature "$sig_file" "$sum_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_deepin()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	wget_file "$url"/{"$sum_file","$iso_file"}
	verify_checksum
	msg "deepin does not provide signature files."
}

download_dragora()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	wget_file "$url/$sum_file"
	verify_checksum
}

download_endeavour()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	verify_checksum
}

download_fedora()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url/$sig_file"
	verify_gpg_signature "$sig_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_ghostbsd()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	wget_file "$url"/{"$sum_file","$iso_file"}
	verify_checksum
	msg "GhostBSD does not provide signature files."
}

download_gparted()
{
	manual_download
	sig_sum_url="$(jq -r ".oddballs.gparted.sig_sum_url" "$json_file")"
	wget_spider "$url/$iso_file" "$sig_sum_url"/{"$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$sig_sum_url"/{"$sig_file","$sum_file"}
	verify_gpg_signature "$sig_file" "$sum_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_guix()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}

	[[ "$traverse_path" == *"latest"* ]] && {
		msg "If the upstream process of making latest images has failed, you can't download those!"
		msg "These images are development snapshots and don't have stability."
		wget_file "$url/$iso_file"
		msg "Guix System does not provide checksum or signature files for this image."

		if [[ "$traverse_path" == *"binary"* ]]; then
			mv "$iso_file" "x86_64-latest-$(cut -d'+' -f4 <<<"$iso_file")"
		else
			mv "$iso_file" "x86_64-latest-guix-$(cut -d'+' -f4 <<<"$iso_file")"
		fi

		return 0
	}

	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	msg "Guix System does not provide checksum files."
}

download_hyperbola()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_gpg_signature "$sig_file" "$sum_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_kali()
{
	if [[ "$traverse_path" == *"weekly"* ]]; then
		html_content="$(wget -qO- "$url/")"
		current_week_num="$(grep -o 'W[0-9]\+' <<<"$html_content" | sort -r | head -n 1 | cut -c2-)"
		iso_file="${iso_file//\{current_week_num\}/$current_week_num}"

		if ! grep -qF "$iso_file" <<<"$html_content"; then
			warn "No new ISO for week: ${yellow}$current_week_num"
			warn "Downloading previous week."
			((prev_week_num = current_week_num - 1))
			iso_file="${iso_file//$current_week_num/$prev_week_num}"
		fi
	fi

	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_gpg_signature "$sig_file" "$sum_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_mint()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_gpg_signature "$sig_file" "$sum_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_mxlinux()
{
	# Different mxlinux DEs ISOs have different GPG keys
	if [[ "$iso_file" == *"Xfce"* ]]; then
		distro=mxlinux_xfce
	elif [[ "$iso_file" == *"KDE"* ]]; then
		distro=mxlinux_kde
	elif [[ "$iso_file" == *"fluxbox"* ]]; then
		distro=mxlinux_fluxbox
	fi

	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}

	[[ "$traverse_path" == *"raspberry_pi"* ]] && {
		wget_file "$url/$iso_file"
		msg "MX Linux does not provide checksum or signature files for this image."
		return 0
	}

	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	verify_checksum
}

download_nix()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	wget_file "$url"/{"$sum_file","$iso_file"}
	printf "\n" >>"$sum_file"
	read -r line <"$sum_file"
	new_iso_file="${line##* }"
	mv "$iso_file" "$new_iso_file"
	iso_file="$new_iso_file"
	verify_checksum
	msg "NixOS does not provide signature files."
}

download_nobara()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	wget_file "$url"/{"$sum_file","$iso_file"}
	verify_checksum
	msg "Nobara does not provide signature files."
}

download_openbsd()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_signify_key
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_signify_signature
	wget_file "$url/$iso_file"
	verify_checksum
	IFS="." read -ra path_parts <<<"$traverse_path"
	architecture="${path_parts[3]}"
	base_name="${iso_file%.*}"
	ext="${iso_file##*.}"
	mv "$iso_file" "${base_name}-${architecture}.${ext}"
}

download_opensuse()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_gpg_signature "$sig_file" "$sum_file"
	read -r line <"$sum_file"
	iso_file="${line##* }"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_parabola()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	wget_file "$url/$sum_file"
	verify_checksum
}

download_parch()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	wget_file "$url"/{"$sum_file","$iso_file"}
	verify_checksum
	msg "Parch GNU/Linux does not provide signature files."
}

download_parrot()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url/$sig_file"
	verify_gpg_signature "$sig_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_pop()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_gpg_signature "$sig_file" "$sum_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_proxmox()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_gpg_signature "$sig_file" "$sum_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_pure()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	wget_file "$url"/{"$sum_file","$iso_file"}
	verify_checksum
	msg "PureOS does not provide signature files."
}

download_qubes()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url/$sum_file"
	verify_gpg_signature "$sum_file"
	wget_file "$url"/{"$sig_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	verify_checksum
}

download_rocky()
{
	[[ "$traverse_path" =~ ${distro}_([0-9]+) ]] && distro="${distro}_${BASH_REMATCH[1]}"

	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}

	[[ "$sig_file" == "null" ]] && {
		wget_file "$url"/{"$sum_file","$iso_file"}
		verify_checksum
		msg "Rocky Linux does not provide signature files for this image."
		return 0
	}

	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_gpg_signature "$sig_file" "$sum_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_slackware()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	verify_checksum
}

download_solus()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_gpg_signature "$sig_file" "$sum_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_tails()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	msg "Tails does not provide checksum files."
}

download_tinycore()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	wget_file "$url"/{"$sum_file","$iso_file"}
	verify_checksum
	msg "Tiny Core does not provide signature files."
}

download_trisquel()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	verify_checksum
}

download_ubuntu()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	[[ "$traverse_path" == *"v14_04"* ]] && fetch_gpg_key "${distro}_v14_04"
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_gpg_signature "$sig_file" "$sum_file"
	wget_file "$url/$iso_file"
	verify_checksum
}

download_void()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_minisign_key
	wget_file "$url"/{"$sig_file","$sum_file"}
	verify_minisign_signature
	wget_file "$url/$iso_file"
	verify_checksum
}

download_whonix()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	fetch_gpg_key "$distro"
	wget_file "$url"/{"$sig_file","$sum_file","$iso_file"}
	verify_gpg_signature "$sig_file" "$iso_file"
	verify_checksum
}

download_zorin()
{
	manual_download
	wget_spider "$url"/{"$iso_file","$sig_file","$sum_file"}
	wget_file "$url/$iso_file"
	msg "Zorin OS does not provide checksum or signature files."
}

toggle_auto_update()
{
	if [[ ! -f "$conf_file" ]]; then
		error "Config file not found."
		msg "Generating a new one..."
		generate_conf_file
	fi

	if [[ "$auto_update_enabled" == "true" ]]; then
		sed -i 's/auto_update_enabled=\(\"true\"\|true\)/auto_update_enabled="false"/' "$conf_file"
		msg "JSON auto-updating: ${red}DISABLED"
	else
		sed -i 's/auto_update_enabled=\(\"false\"\|false\)/auto_update_enabled="true"/' "$conf_file"
		msg "JSON auto-updating: ${green}ENABLED"
	fi
}

generate_conf_file()
{
	[[ -f "$conf_file" ]] && {
		local choice

		warn "This will overwrite the config file with default values."

		read -rp "${bold}Continue? (y/N):$reset " choice

		[[ "${choice,,}" =~ ^(y|yes)$ ]] || {
			msg "Canceled."
			return 1
		}
	}

	{
		# shellcheck disable=SC2016
		printf 'default_download_dir="$PWD"\n'
		printf 'auto_update_enabled="true"\n'
		printf 'verification_enabled="true"\n'
		printf 'spider_mode_enabled="false"\n'
		printf 'manual_download_enabled="false"\n'
		printf 'gpg_keyserver="hkps://keyserver.ubuntu.com"\n'
	} >"$conf_file"

	msg "Made config file at: ${green}$conf_file"
}

delete_gpg_key()
{
	dependency_check "gpg"

	gpg_key_id="${1:-}"

	if [[ -z "$gpg_key_id" ]]; then
		die "A GPG key ID is required."
	fi

	if gpg --homedir "$gpg_dir" --yes --batch --delete-keys "$gpg_key_id" &>/dev/null; then
		msg "GPG key deletion: ${green}SUCCESS"
	else
		die "GPG key deletion: ${red}FAILED"
	fi
}

import_gpg_key()
{
	dependency_check "gpg"

	gpg_key_file="${1:-}"

	if [[ -z "$gpg_key_file" ]]; then
		die "A path to a GPG key file is required."
	fi

	if gpg --homedir "$gpg_dir" --import "$gpg_key_file" &>/dev/null; then
		msg "GPG key import: ${green}SUCCESS"
	else
		die "GPG key import: ${red}FAILED"
	fi
}

list_gpg_keys()
{
	dependency_check "gpg"

	list_keys_output="$(gpg --homedir "$gpg_dir" --list-keys 2>/dev/null)"

	if [[ -z "$list_keys_output" ]]; then
		msg "No GPG keys saved."
	else
		printf "%s\n" "$list_keys_output"
	fi
}

purge_all_data()
{
	local choice

	warn "This will remove ALL stored data at: ${yellow}$data_dir"

	read -rp "${bold}Continue? (y/N):$reset " choice

	[[ "${choice,,}" =~ ^(y|yes)$ ]] || {
		msg "Canceled."
		return 1
	}

	rm -rf -- "$data_dir"

	msg "Data purge: ${green}SUCCESS"
}

update_json_file()
{
	temp_json_file="$json_dir/.temp_data.json"

	if wget -O "$temp_json_file" -q "$json_url"; then
		mv "$temp_json_file" "$json_dir/data.json"
		msg "JSON file update: ${green}SUCCESS"
	else
		rm -f "$temp_json_file"
		die "JSON file update: ${red}FAILED"
	fi
}

list_distros()
{
	printf "SUPPORTED DISTRIBUTIONS:\n"
	printf "%s\n" "$(printf "  %s\n" "${supported_distros[@]}" | sort)"
}

usage()
{
	cat <<EOF
USAGE:
  dl-distro [OPTIONS]
  dl-distro -d <DISTRO|JSON_QUERY> [OPTIONS]

OPTIONS:
  -a, --auto-update                          Toggle auto-updating of the local JSON file.
  -c, --conf-file                            Generate a config file with default values.
  -d, --distro <DISTRO|JSON_QUERY>           Specify a distro by name or a JSON query.
  -D, --delete-key <KEY_ID>                  Delete a stored GPG key using its key ID.
  -h, --help                                 Display this help message.
  -i, --import-key <KEY_FILE>                Import a GPG key from a key file.
  -l, --list-keys                            List the stored GPG keys.
  -L, --list-distros                         List the supported distributions.
  -m, --manual-download <DISTRO|JSON_QUERY>  Show manual download steps
  -n, --no-verify                            Skip key fetching and verification.
  -p, --path <PATH>                          Download image to specified directory.
  -P, --purge-data                           Purge ALL the stored data.
  -s, --spider                               Return HTTP status code for image's URLs.
  -u, --update-json                          Update the locally stored JSON file.
  -V, --version                              Print dl-distro's current version.

EXAMPLES:
  dl-distro
  dl-distro -d debian
  dl-distro -d debian.netinst.amd64 -p ~/Downloads
  dl-distro -m
  dl-distro -m debian.netinst.amd64
EOF
}

cleanup()
{
	[[ "$spider_mode_enabled" == "true" ]] && return 0
	[[ "$sig_file" != "null" ]] && rm -f "$sig_file"
	[[ "$sum_file" != "null" ]] && rm -f "$sum_file"
	return 0
}

start_download()
{
	mapfile -t json_values < <(jq -r "$traverse_path | .url, .iso_file, .sig_file, .sum_file" "$json_file")

	url="${json_values[0]}"
	iso_file="${json_values[1]}"
	sig_file="${json_values[2]}"
	sum_file="${json_values[3]}"
	sum_algo="$(jq -r ".sum_algos.$distro" "$json_file")"

	[[ "$manual_download_enabled" != "true" && "$spider_mode_enabled" != "true" ]] && {
		trap cleanup EXIT
		msg "Downloading at: ${green}$default_download_dir"
	}

	download_"$distro"

	exit 0
}

traverse_json_file()
{
	traverse_path="$1"

	if jq -e "$traverse_path.url" "$json_file" &>/dev/null; then
		start_download
	fi

	if [[ -n "${json_query:-}" ]]; then
		die "Invalid JSON query: ${red}${traverse_path#.}"
	fi

	mapfile -t json_keys < <(jq -r "$traverse_path | keys | .[]" "$json_file")

	json_options=("${json_keys[@]}" "back")

	if [[ "${#json_options[@]}" -le 1 ]]; then
		error "JSON parsing error."
		die "Try updating the JSON file, else report on Codeberg."
	fi

	printf "\e[2J\e[H"

	msg "${und}$distro_pretty_name"

	select json_choice in "${json_options[@]}"; do
		case "$json_choice" in
			"back")
				if [[ "$traverse_path" == ".$distro" ]]; then
					main_menu
				else
					traverse_json_file "${traverse_path%.*}"
				fi
				;;
			*)
				if [[ -z "$json_choice" ]]; then
					error "Invalid option: ${red}$REPLY"
					continue
				else
					traverse_json_file "$traverse_path.$json_choice"
				fi
				;;
		esac
	done
}

main_menu()
{
	mapfile -t sorted_distros < <(printf "%s\n" "${!supported_distros[@]}" | sort)

	printf "\e[2J\e[H"

	msg "${und}Main Menu"

	select main_menu_choice in "${sorted_distros[@]}"; do
		if [[ -z "$main_menu_choice" ]]; then
			error "Invalid option: ${red}$REPLY"
			continue
		fi

		distro="${supported_distros["$main_menu_choice"]}"
		distro_pretty_name="$main_menu_choice"

		wait

		traverse_json_file ".$distro"
	done
}

cli_mode()
{
	if [[ "$d_option_arg" == *.* ]]; then
		json_query="$d_option_arg"
		distro="${d_option_arg%%.*}"
	else
		distro="$d_option_arg"
	fi

	for key in "${!supported_distros[@]}"; do
		if [[ "${supported_distros[$key]}" == "$distro" ]]; then
			distro_pretty_name="$key"
			break
		fi
	done

	if type download_"$distro" &>/dev/null; then
		wait

		traverse_json_file ".${json_query:-$distro}"
	else
		die "Invalid distro: ${red}$distro"
	fi
}

parse_opts()
{
	cli_mode=0

	while (($#)); do
		case "$1" in
			-a | --auto-update)
				toggle_auto_update
				exit 0
				;;
			-c | --conf-file)
				generate_conf_file
				exit 0
				;;
			-d | --distro)
				if [[ -n "${2:-}" ]]; then
					d_option_arg="$2"
					cli_mode=1
					shift 2
				else
					die "$1 requires a distro name or JSON query."
				fi
				;;
			-D | --delete-key)
				delete_gpg_key "${2:-}"
				exit 0
				;;
			-h | --help)
				usage
				exit 0
				;;
			-i | --import-key)
				import_gpg_key "${2:-}"
				exit 0
				;;
			-l | --list-keys)
				list_gpg_keys
				exit 0
				;;
			-L | --list-distros)
				list_distros
				exit 0
				;;
			-m | --manual-download)
				manual_download_enabled="true"
				if [[ -n "${2:-}" ]]; then
					d_option_arg="$2"
					cli_mode=1
					shift 2
				else
					shift 1
				fi
				;;
			-n | --no-verify)
				verification_enabled="false"
				shift 1
				;;
			-p | --path)
				if [[ -n "${2:-}" ]]; then
					default_download_dir="$2"
					shift 2
				else
					die "$1 requires a directory path."
				fi
				;;
			-P | --purge-data)
				purge_all_data
				exit 0
				;;
			-s | --spider)
				spider_mode_enabled="true"
				shift 1
				;;
			-u | --update-json)
				update_json_file
				shift 1
				(($# == 0)) && exit 0
				;;
			-V | --version)
				printf "%s\n" "$version"
				exit 0
				;;
			*)
				die "Invalid option: ${red}$1"
				;;
		esac
	done
}

main()
{
	((EUID == 0)) && die "Do not run as root."

	dependency_check "jq" "wget"

	mkdir -p "$conf_dir" "$json_dir" "$gpg_dir" "$minisign_dir" "$signify_dir"
	chmod 700 "$gpg_dir"

	parse_opts "$@"

	if [[ ! -f "$json_file" || "$auto_update_enabled" == "true" ]]; then
		update_json_file >/dev/null &
	fi

	mkdir -p "$default_download_dir"
	cd "$default_download_dir"

	if [[ "$cli_mode" = 0 ]]; then
		main_menu
	else
		cli_mode
	fi
}

main "$@"
