#!/bin/bash
# Antigraphon - from Ancient Greek ἀντίγραφον; "A manuscript from which a copy is made"
# /boot/efi - VFAT
# swap      - RAM size
# btrfs     - all other size
#
# root is btrfs, home - /home
# 
# Scheme:
#  check if layout is OK
#  Create new GPT table
#  create FAT32 - 250 MB
#  mount FAT32, rsync orig disk to FAT32
#  create BTRFS - other Gb
#  create swap - 16 Gb

if [[ "$1" == "--installed--" ]]; then
	shift 1
	PKG_INSTALLED=1
else
	PKG_INSTALLED=0
fi

xcomp="$(realpath "$0" | grep -Eo '^/home/[^/]+/')" # find HOME directory if no other methods worked (only script downloaded)
if [[ -n "$xcomp" ]]; then
	xname="${xcomp#/home/}" # find username of pkexec user (?)
	xname="${xname%%/*}"    # cut everything after username/*
fi

_notify-send() { # wrapper for notify-send
	if (( PKG_INSTALLED )); then
		notify-send "$@"
	else
		if [[ -n "$SUDO_USER" ]]; then
			_uid="$(id -u "$SUDO_USER")"
			sudo -u "$SUDO_USER" DBUS_SESSION_BUS_ADDRESS=unix:path="/run/user/$_uid/bus" DISPLAY="$DISPLAY" notify-send "$@"
		elif [[ -n "$PKEXEC_USER" ]]; then
			_pkname="$(id -nu "$PKEXEC_USER")"
			sudo -u "$_pkname" DBUS_SESSION_BUS_ADDRESS=unix:path="/run/user/$PKEXEC_USER/bus" DISPLAY="$DISPLAY" notify-send "$@"
		elif [[ -n "$xname" ]]; then
			_uid="$(id -u "$xname")"
			sudo -u "$xname" DBUS_SESSION_BUS_ADDRESS=unix:path="/run/user/$_uid/bus" DISPLAY="$DISPLAY" notify-send "$@"
		else
			notify-send "$@"
		fi
	fi
}

onexit() {
	[[ -z "$easter"    ]] || rm -f "$easter"
	[[ -z "$log"       ]] || rm -f "$log"
	[[ -z "$iserror_g" ]] || rm -f "$iserror_g"
	[[ -z "$iserror"   ]] || rm -f "$iserror"
	[[ -z "$forkvars"  ]] || { source "$forkvars"; rm -f "$forkvars"; }
	[[ -z "$oldtmp"    ]] || if mountpoint -q "$oldtmp"; then umount -R "$oldtmp"; else rm -r -f "$oldtmp"; fi
	[[ -z "$newtmp"    ]] || if mountpoint -q "$newtmp"; then umount -R "$newtmp"; else rm -r -f "$newtmp"; fi
	[[ -z "$chroot"    ]] || if mountpoint -q "$chroot"; then umount -R "$chroot"; else rm -r -f "$chroot"; fi
}
trap onexit EXIT

# Check root
if [[ "$EUID" != 0 ]]; then
	if (( PKG_INSTALLED )); then
		yad --error --text="Этот скрипт должен запускаться с root правами!" --title="Ошибка"
	else
		if command -v pkexec &>/dev/null; then
			chmod u+x "$(realpath "$0")" # maybe user executed script via bash $0...
			exec pkexec env DISPLAY="$DISPLAY" XAUTHORITY="$XAUTHORITY" "$(realpath "$0")"
			exit 0
		elif command -v yad &>/dev/null; then
			yad --error --text="Этот скрипт должен запускаться с root правами!" --title="Ошибка"
			exit 1
		fi
	fi
fi

# Helper function for the code below
find_terminal() {
    # Detect desktop environment
    local de="${XDG_CURRENT_DESKTOP:-${XDG_SESSION_DESKTOP:-}}"
    de="${de,,}"  # lowercase
    
    # Define terminals per DE that support -e
    local -A de_terms=(
        [xfce]=xfce4-terminal
        [kde]=konsole
        [plasma]=konsole
        [gnome]=gnome-terminal
        [gnome-classic]=gnome-terminal
        [ubuntu]=gnome-terminal
        [cinnamon]=gnome-terminal
        [mate]=mate-terminal
        [lxde]=lxterminal
        [lxqt]=qterminal
        [pantheon]=pantheon-terminal
        [budgie]=tilix
        [deepin]=deepin-terminal
        [enlightenment]=terminology
        [i3]=i3-sensible-terminal
        [sway]=foot
        [hyprland]=foot
    )
    
    # Fallback list of all terminals supporting -e
    local fallback=(
        xfce4-terminal konsole gnome-terminal kgx mate-terminal
        lxterminal qterminal pantheon-terminal tilix terminology
        foot alacritty kitty urxvt st xterm
    )
    
    # Try DE-specific first
    for t in ${de_terms[$de]} ${de_terms[${de%%:*}]}; do
        command -v "$t" >/dev/null 2>&1 && { echo "$t"; return 0; }
    done
    
    # Fallback to common terminals
    for t in "${fallback[@]}"; do
        command -v "$t" >/dev/null 2>&1 && { echo "$t"; return 0; }
    done
    
    return 1
}

if (( ! PKG_INSTALLED )); then
	# Check binaries
	missing=()
	for _bin in btrfs:btrfs-progs yad:yad mkfs.vfat:dosfstools sgdisk:gptfdisk partprobe:parted pkexec:polkit notify-send:libnotify; do
		_com="${_bin%%:*}"
		_pkg="${_bin##*:}"
		if ! command -v "$_com" &>/dev/null; then
			missing+=("$_pkg")
		fi
	done
	easter="$(mktemp)"
	echo 0 > "$easter"
	if (( ${#missing[@]} )); then
		terminal="$(find_terminal)"
		if [[ -n "$terminal" ]]; then
			"$terminal" -e bash -c "if [[ \"\$EUID\" == 0 ]]; then dnf install -y ${missing[*]}; else if command -v pkexec &>/dev/null; then pkexec dnf install -y ${missing[*]}; else echo -e '\n\e[1;33mСейчас будут установлены дополнительные пакеты, введите пароль администратора.\e[0m'; sudo dnf install -y ${missing[*]}; fi; fi || { echo 1 > /$easter; sleep 5; }"
		else
			if [[ -t 0 ]] || tty -s &>/dev/null; then # check if we executed inside terminal
				bash -c "if [[ \"\$EUID\" == 0 ]]; then dnf install -y ${missing[*]}; else if command -v pkexec &>/dev/null; then pkexec dnf install -y ${missing[*]}; else echo -e '\n\e[1;33mСейчас будут установлены дополнительные пакеты, введите пароль администратора.\e[0m'; sudo dnf install -y ${missing[*]}; fi; fi || { echo 1 > /$easter; sleep 5; }"
			else
				echo 1 > "$easter"
			fi
		fi
		if [[ "$(<"$easter")" == "1" ]]; then
			if command -v yad &>/dev/null; then
				yad --error --text="Установите: ${missing[@]}, используя dnf" --title="Ошибка"
			elif command -v notify-send &>/dev/null; then
				_notify-send -u critical -a "${0##*/}" "Ошибка!" "Установите: ${missing[@]}, используя dnf"
			else
				echo -e "\n\e[1;31mОШИБКА!\e[0m Установите: ${missing[@]}, используя dnf" >&2
			fi
			exit 1
		else
			if [[ "$EUID" != 0 ]]; then
				rm "$easter"; easter=""
				chmod u+x "$(realpath "$0")"
				exec pkexec env DISPLAY="$DISPLAY" XAUTHORITY="$XAUTHORITY" "$(realpath "$0")"
				exit 0
			fi
		fi
	fi
fi

# Check UEFI
if ! mountpoint -q /sys/firmware/efi/efivars; then
	yad --error --text="Конфигурация не поддерживается!\n\nВаша система не загружена через UEFI" --title="Ошибка" --width=500
	exit 1
fi

# Check if / or /home is btrfs
_btrfs_root_check="$(findmnt -n -o FSTYPE -T /)"
if [[ "$_btrfs_root_check" != "btrfs" ]]; then
	yad --error --text="Конфигурация не поддерживается!\n\nКорневой раздел диска не использует BTRFS" --title="Ошибка" --width=500
	exit 1
fi
if mountpoint -q /home; then
	_btrfs_home_check="$(findmnt -n -o FSTYPE -T /home)"
	if [[ "$_btrfs_home_check" != "btrfs" ]]; then
		yad --error --text="Конфигурация не поддерживается!\n\n/home является точкой монтирования, но используемая ФС это не BTRFS" --title="Ошибка" --width=500
		exit 1
	fi
fi

# Pretty disk list, for showing in GUI
pretty_disks="$(lsblk -d -n -p -o NAME,SIZE,MODEL)"
# Internal-use disk list, in /dev/sdX format
mapfile -t disks < <(lsblk -d -n -p -r -o NAME)
# Add [SIZE]
pretty_disks="$(echo "$pretty_disks" | while read -r line; do
			echo "$line" | sed -E "s/^([^[:space:]]+)([[:space:]]+)([^[:space:]]+)(.*)$/\1\2[\3]\4/"
		done)"
# line count for both lists
pd_lc="$(echo "$pretty_disks" | wc -l)"
 d_lc="${#disks[@]}"
# Terminate is disk counter differs (user will just relaunch an app)
if (( pd_lc != d_lc )); then
	yad --error --text="Вы в неудобный момент вынули флешку.\nВставьте её обратно и перезапустите приложение" --title="Ошибка" --width=500
	exit 1
fi
# Show main window
result="$(yad --form \
	--title="Клонирование системы" \
	--text="Выберите целевой диск для клонирования" \
	--field="Целевой диск:CB" \
	"$pretty_disks" \
	--button="Начать клонирование:0" \
	--button="Выход:1" \
	--buttons-layout=center \
	--item-separator=$'\n' \
	--width=400 --height=200 \
	--num-output
)"
_exit="$?"
if (( _exit )); then
	exit 0
fi
# Read chosen disks
mapfile -t -d "|" choice < <(echo "$result")
# Set raw disks correlated to pretty disks
dest="${disks[$((${choice[0]}-1))]}"
# Find orig disk that already mounted (booted)
mounted_root="$(findmnt -n -o SOURCE -v -T /)"
mounted_root_options="$(findmnt -n -o OPTIONS -T /)"
mounted_root_options_grep="$(echo "$mounted_root_options" | grep -E -o '^subvol=[^,]+$|^subvol=[^,]+,|,subvol=[^,]+$|,subvol=[^,]+,')" # find subvol= field
if [[ -n "$mounted_root_options_grep" ]]; then
	mounted_root_subvol="$(echo "$mounted_root_options_grep" | sed 's/^,//;s/,$//' | sed 's/^subvol=//')" # cut first and last `,` symbol
	case "$mounted_root_subvol" in /*) : ;; *) mounted_root_subvol="/$mounted_root_subvol" ;; esac
else
	yad --error --text="Текущая разметка диска не поддерживается!\n\nОтсутствует подтом btrfs для /" --title="Ошибка"
	exit 1
fi
mounted_root_uuid="$(lsblk -d -n -p -r -o UUID "$mounted_root")"
disk_from_root="$(lsblk -d -n -p -r -s -o NAME "$mounted_root" | sed '$!d')"
# Check if disks are the same
if [[ "$dest" == "$disk_from_root" ]]; then
	yad --error --text="Исходный и целевой диски должны быть разными!" --title="Ошибка"
	exit 1
fi
# Find /home (if it exists as another subvol/mountpoint)
mounted_home="$(findmnt -n -o SOURCE -v -T /home)"
if (( $? )) || [[ -z "$mounted_home" ]]; then
	home_on_other_subvol=0
else
	mounted_home_options="$(findmnt -n -o OPTIONS -T /home)"
	mounted_home_options_grep="$(echo "$mounted_home_options" | grep -E -o '^subvol=[^,]+$|^subvol=[^,]+,|,subvol=[^,]+$|,subvol=[^,]+,')" # find subvol= field
	if [[ -n "$mounted_home_options_grep" ]]; then
		mounted_home_subvol="$(echo "$mounted_home_options_grep" | sed 's/^,//;s/,$//' | sed 's/^subvol=//')" # cut first and last `,` symbol
		case "$mounted_home_subvol" in /*) : ;; *) mounted_home_subvol="/$mounted_home_subvol" ;; esac # sane formatting
	else
		yad --error --text="Текущая разметка диска не поддерживается!\n\nОтсутствует подтом btrfs для /home" --title="Ошибка"
		exit 1
	fi
	mounted_home_uuid="$(lsblk -d -n -p -r -o UUID "$mounted_home")"
	home_on_other_subvol=1
	if [[ "$mounted_home" == "$mounted_root" ]]; then
		home_subvol_same_partition=1
	else
		home_subvol_same_partition=0
	fi
fi
# Check sizes
dest_size="$(lsblk -d -n -r -b -o SIZE "$dest")"
root_size="$(lsblk -d -n -r -b -o FSUSED "$mounted_root")"
if (( home_on_other_subvol )); then
	if (( home_subvol_same_partition )); then
		home_size=0
	else
		home_size="$(lsblk -d -n -r -b -o FSUSED "$mounted_home")"
	fi
else
	home_size=0
fi
efi_size=262144000    # 250 MB / 1024 * 1024 * 250       bytes
swap_size=17179869184 # 16 GB  / 1024 * 1024 * 1024 * 16 bytes
total_size="$((root_size + home_size + efi_size + swap_size))"
if (( total_size > dest_size )); then
	yad --error --text="На целевом диске на хватает места!" --title="Ошибка"
	exit 1
:
fi
# Ask user
yad --question --text="Внимание! Все данные на диске '$dest' будут уничтожены.\n\nПродолжить?" --title="Подтверждение" --width=500 --height=100
_exit="$?"
if (( _exit )); then
	exit 0
fi
log="$(mktemp)"       # Main program log
iserror_g="$(mktemp)" # Simple error checker (grace)
iserror="$(mktemp)"   # Error checker (because we running in subshell down below)
forkvars="$(mktemp)"  # Export $oldtmp/$newtmp/$chroot directories from subshell to mainshell (for EXIT trap)
echo 0 > "$iserror_g"
echo 0 > "$iserror"
( # Note for reviewers: no, this subshell doesn't need a separate exit status (because of | yad pipe). All checks are done with $iserror, some commands may fail, its ok.
:::() { # dummy function for 'beauty'
	echo "$@"
}
---() { # logger
	echo "===== $(date) =====" &>>$log
	echo ">>>>> $@" &>>$log
	"$@" &>>$log || {
		echo 1 > "$iserror"
		::: 100
		exit 1
	}
}
# Format disk (GPT table)
echo "# Форматирование диска..."
::: 10
--- sgdisk --zap-all "$dest"
::: 12
--- sgdisk --clear   "$dest"
::: 13
--- sgdisk --new=1:0:+250M --typecode=1:ef00 --change-name=1:"EFI System" "$dest"
::: 14
--- sgdisk --new=2:0:-16G  --typecode=2:8300 --change-name=2:"root"       "$dest"
::: 15
--- sgdisk --new=3:0:0     --typecode=3:8200 --change-name=3:"swap"       "$dest"
::: 17
--- partprobe "$dest"
::: 19
# Filesystems
# Get parsable partitions RIGHT way
# (for NVME they might be /dev/nvme1n1 not /dev/sda1 so we would add n1 not 1 at the end)
#############################################
dest_table="$(lsblk -n -p -r -o NAME "$dest" | sed '1d')"
if [[ "$(echo "$dest_table" | wc -l)" != 3 ]]; then
	yad --error --text="Диск был неправильно размечен!" --title="Ошибка"
	echo 1 > "$iserror_g"
	exit 1
fi
dest_efi="$(echo  "$dest_table" | sed '1!d')"
dest_root="$(echo "$dest_table" | sed '2!d')"
dest_swap="$(echo "$dest_table" | sed '3!d')"
#############################################
echo "# Создание файловых систем..."
::: 20
umount "$dest_efi"  &>/dev/null || :
--- mkfs.vfat -F32 "$dest_efi" 
::: 25
umount "$dest_root" &>/dev/null || :
--- mkfs.btrfs -f  "$dest_root"
::: 30
umount "$dest_swap" &>/dev/null || :
--- mkswap         "$dest_swap"
::: 35
# Extract new UUIDs (they were changed while formatting partitions)
#############################################
partprobe "$dest"
sleep 3
dest_table_uuid="$(lsblk -n -p -r -o UUID "$dest" | sed '1d')"
if [[ "$(echo "$dest_table_uuid" | wc -l)" != 3 ]]; then
	yad --error --text="Диск был неправильно отформатирован!" --title="Ошибка"
	echo 1 > "$iserror_g"
	exit 1
fi
dest_efi_uuid="$(echo  "$dest_table_uuid" | sed '1!d')"
dest_root_uuid="$(echo "$dest_table_uuid" | sed '2!d')"
dest_swap_uuid="$(echo "$dest_table_uuid" | sed '3!d')"
#############################################
::: 37
echo "# Подготовка к копированию..."
::: 40
oldtmp="$(mktemp -d)" || exit 1
declare -p oldtmp >> "$forkvars"
--- mount "$mounted_root" "$oldtmp"
::: 41
if [[ -d "$oldtmp/root_transfer" ]]; then
	--- btrfs subvol delete "$oldtmp/root_transfer"
	::: 42
fi
--- btrfs subvol snap -r "${oldtmp}${mounted_root_subvol}" "$oldtmp/root_transfer" # Note for reviewers: we already check command status based on --- logger function above
::: 43
if (( home_on_other_subvol )); then
	if [[ -d "$oldtmp/home_transfer" ]]; then
		--- btrfs subvol delete "$oldtmp/home_transfer"
		::: 44
	fi
	--- btrfs subvol snap -r "${oldtmp}${mounted_home_subvol}" "$oldtmp/home_transfer"
	::: 45
fi
echo "# Копирование данных..."
::: 50
newtmp="$(mktemp -d)" || exit 1
declare -p newtmp >> "$forkvars"
--- mount "$dest_root" "$newtmp"
::: 51
    btrfs send --compressed-data "$oldtmp/root_transfer" | --- btrfs receive "$newtmp"
::: 52
if (( home_on_other_subvol )); then
	btrfs send --compressed-data "$oldtmp/home_transfer" | --- btrfs receive "$newtmp"
	::: 55
fi
echo "# Синзронизация снапшотов..."
::: 60
--- btrfs subvol snap "$newtmp/root_transfer" "$newtmp/root"
::: 61
if (( home_on_other_subvol )); then
---	btrfs subvol snap "$newtmp/home_transfer" "$newtmp/home"
:::	65
fi
echo "# Удаление снапшотов..."
::: 70
--- btrfs subvol delete "$newtmp/root_transfer"
if (( home_on_other_subvol )); then
---	btrfs subvol delete "$newtmp/home_transfer"
:::	75
fi
--- umount "$oldtmp"
::: 77
--- umount "$newtmp"
::: 79
echo "# Настройка системы..."
::: 80
chroot="$(mktemp -d)" || exit 1
declare -p chroot >> "$forkvars"
--- mount -o subvol=/root "$dest_root" "$chroot"
::: 81
if [[ ! -d "$chroot/boot/efi" ]]; then
	mkdir -p "$chroot/boot/efi"
	::: 82
fi
--- mount "$dest_efi" "$chroot/boot/efi"
::: 83
if (( home_on_other_subvol )); then
	if [[ ! -d "$chroot/home" ]]; then
		mkdir "$chroot/home"
		::: 84
	fi
	--- mount -o subvol=/home "$dest_root" "$chroot/home"
	::: 85
fi
for sdir in /dev /proc /sys /sys/firmware/efi/efivars; do
	if test -d "$sdir"; then
		if [[ ! -d "${chroot}${sdir}" ]]; then
			mkdir -p "${chroot}${sdir}"
		fi
		--- mount --bind "$sdir" "${chroot}${sdir}"
	fi
done
::: 86
cat <<EOF > "$chroot/etc/fstab"
UUID=$dest_root_uuid		/		btrfs	defaults,subvol=/root		1	1
UUID=$dest_efi_uuid		/boot/efi	vfat	umask=0077,shortname=winnt	0	2
UUID=$dest_swap_uuid		none		swap	defaults			0	0
EOF
if (( home_on_other_subvol )); then
cat <<EOF >> "$chroot/etc/fstab"
UUID=$dest_root_uuid		/home		btrfs	defaults,subvol=/home		0	2
EOF
fi
::: 88
--- chroot "$chroot" grub2-install
::: 90
BUILD_ID="$(. "$chroot"/var/lib/rosa-iso-info && echo "$BUILD_ID")" # crutch for beautiful ROSA scripts
BUILD_ID="${BUILD_ID:-0}"
--- echo "$BUILD_ID" > "$chroot/boot/efi/rosa-iso-build"
--- chroot "$chroot" grub2-install-hooks
::: 91
--- chroot "$chroot" update-grub2
::: 92
if [[ -e "$chroot/var/lib/original-machine-id" ]]; then # if original /etc/machine-id already saved, just recreate this file
--- chroot "$chroot" rm -f /etc/machine-id
else                                                    # else, save /etc/machine-id to /var/lib/original-machine-id
--- chroot "$chroot" mv -f /etc/machine-id /var/lib/original-machine-id
fi
::: 93
--- chroot "$chroot" systemd-machine-id-setup
::: 95
for sdir in /boot/efi /sys/firmware/efi/efivars /sys /proc /dev; do
	if test -d "${chroot}${sdir}"; then
		umount "${chroot}${sdir}"
	fi
done
::: 97
--- umount -R "$chroot"
::: 100
# Main script
) | yad --progress --title="Копирование..." --text="Окно закроется автоматически" --auto-close --percentage=0 --width=500 --height=100

if [[ "$(<"$iserror_g")" == "1" ]]; then
	exit 1
fi

if [[ "$(<"$iserror")" == "1" ]]; then
	if [[ -n "$PKEXEC_USER" ]]; then
		save_to="$(getent passwd "$PKEXEC_USER" | cut -d ":" -f 6)/btrfs-$(date +"%H:%M_%d-%m-%Y").log"
	elif [[ -n "$SUDO_USER" ]]; then
		save_to="$(getent passwd "$SUDO_USER" | cut -d ":" -f 6)/btrfs-$(date +"%H:%M_%d-%m-%Y").log"
	elif [[ -n "$xcomp" ]]; then
		save_to="${xcomp}btrfs-$(date +"%H:%M_%d-%m-%Y").log"
	elif [[ -n "$HOME" ]]; then
		save_to="$HOME/btrfs-$(date +"%H:%M_%d-%m-%Y").log"
	else
		save_to="/tmp/btrfs-$(date +"%H:%M_%d-%m-%Y").log"
	fi
	cp "$log" "$save_to"
	if [[ -n "$PKEXEC_USER" ]]; then
		chown "$PKEXEC_USER:" "$save_to"
	elif [[ -n "$SUDO_USER" ]]; then
		chown "$SUDO_USER:" "$save_to"
	elif [[ -n "$xname" ]]; then
		chown "$xname:" "$save_to"
	fi
	yad --text-info \
	    --title="Произошла ошибка; журнал сохранён в '$save_to'; см. лог ниже:" \
	    --filename="$log" \
	    --width=800 \
	    --height=500
	exit 1
fi

# Note for reviewers: no, we can't show popup with another button on the screen that needs to be clicked. User is already frustrated and want to unplug drive ASAP
_notify-send -t 10000 -u normal -a "${0##*/}" "Готово" "Диск успешно клонирован"

exit 0
