#!/bin/bash

## Copyright (C) 2024, 2025 Ajrat Makhmutov <rauty@altlinux.org>
##
## This file is part of alterator-kopidel.
##
## alterator-kopidel 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.
##
## alterator-kopidel 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 alterator-kopidel.
## If not, see <https://www.gnu.org/licenses/>.

. "/usr/lib/alterator-kopidel/percentage-handlers"
. alterator-sh-functions

DATA_DIR="$(rpm --eval %_datadir)/alterator-kopidel"
GRUB_i386PC_LIBDIR="$(rpm --eval %_libdir)/grub/i386-pc"
IGNORED_FILES="$(rpm --eval %_localstatedir)/alterator-kopidel/ignored-files.txt"
BOOT_MODE=$([ -d /sys/firmware/efi ] && echo UEFI || echo legacy)
readonly DATA_DIR
readonly GRUB_i386PC_LIBDIR
readonly IGNORED_FILES
readonly BOOT_MODE
readonly ALWAYS_IGNORED_FILES="$DATA_DIR/always-ignored-files.txt"
readonly WORKDIR_NAME='alterator-kopidel-workdir'
readonly UUID_REGEX='\./[0-9]\{4\}\(-[0-9]\{2\}\)\{6\}'

STEPS=()
SUBSTEPS=()
run_step() (
	step="$1"
	echo "step start: $step" >> "$in_workdir/build.log"
	echo "step_progress: 0"

	cd "$in_workdir" || exit 1
	"$step" 2>&1 | tee -a "$in_workdir/build.log" | grep -Ex --line-buffered 'step_progress: [0-9]*'

	# ${PIPESTATUS[0]} returns step exit code.
	if [ "${PIPESTATUS[0]}" -ne 0 ]; then
		return 1
	fi

	echo "step_progress: 100"
	echo "step end: $step" >> "$in_workdir/build.log"
)

SUBSTEPS+=( prepare_workdir::create_initrd_image )
prepare_workdir::create_initrd_image() (
	pushd image || return 1

	mkdir -pv .disk
	cp -vf "$DATA_DIR/initrd.mk" .disk/
	cp -vf "$DATA_DIR/bootchain" .disk/

	mkdir -pv boot
	rm -vf boot/initrd*.img
	make-initrd --config=.disk/initrd.mk --bootdir=boot/ BOOTCHAIN_PATH=.disk -v
)

# output: $in_workdir/image/boot/grub/i386-pc/, $in_workdir/image/boot/grub/bios.img
SUBSTEPS+=( prepare_workdir::create_grub_image )
prepare_workdir::create_grub_image() (
	mkdir -pv image/boot/grub/i386-pc
	mkdir -pv image/boot/grub/fonts
	mkdir -pv image/EFI/BOOT
	pushd image || return 1

	cp "$GRUB_i386PC_LIBDIR/"*.mod boot/grub/i386-pc/
	cp -fv /boot/grub/fonts/unicode.pf2 boot/grub/fonts/

	uuid_iso=$(date -u +%Y-%m-%d-%H-%M-%S-00)
	find . -maxdepth 1 -type f -regextype sed -regex "$UUID_REGEX" -delete
	touch "$uuid_iso"

	cp  -f "$DATA_DIR/grub.cfg"         boot/grub.cfg
	cp  -f "$DATA_DIR/grub-efi.cfg"     EFI/BOOT/grub.cfg

	sed -i "s/@KFLAVOUR@/$(uname -r)/g" boot/grub.cfg EFI/BOOT/grub.cfg
	sed -i "s/@UUID@/$uuid_iso/g"       boot/grub.cfg EFI/BOOT/grub.cfg

	menuentry_install="$(_ 'Install a "razlivochniy obraz".')"
	sed -i "s/@MENUENTRY_INSTALL@/$menuentry_install/g" boot/grub.cfg EFI/BOOT/grub.cfg

	modules="linux normal iso9660 f2fs ext2 fat exfat ntfs ntfscomp biosdisk part_gpt search"
	grub-mkstandalone \
		--format="i386-pc" \
		--output=boot/core.img \
		--install-modules="$modules tar ls minicmd reboot halt cat" \
		--modules="$modules" \
		--locales="" \
		--fonts="" \
		"boot/grub/grub.cfg=boot/grub.cfg"

	cat "$GRUB_i386PC_LIBDIR/cdboot.img" boot/core.img > boot/grub/bios.img

	# cleanup temporary files
	rm -v boot/core.img
)

# output: $in_workdir/image/EFI/BOOT/efiboot.img
SUBSTEPS+=( prepare_workdir::create_uefi_esp_image )
prepare_workdir::create_uefi_esp_image() (
	pushd image || return 1
	mkdir -vp EFI/BOOT
	rm    -vf EFI/BOOT/efiboot.img

	if [ "$BOOT_MODE" == legacy ]; then
		# Then let's just create an empty file to avoid unnecessary code.
		dd if=/dev/zero of=EFI/BOOT/efiboot.img bs=512 count=100
		return 0
	fi
	imgsize="$(( $(du -lsB32k /boot/efi/EFI/BOOT/ | cut -f1) + 10 ))"
	dd if=/dev/zero of=EFI/BOOT/efiboot.img bs=32k count="$imgsize"

	# create an MS-DOS FAT filesystem
	/sbin/mkfs.fat -v -F 12 -n 'EFIBOOTISO' EFI/BOOT/efiboot.img

	# create directory structure
	mmd -i EFI/BOOT/efiboot.img ::EFI
	mmd -i EFI/BOOT/efiboot.img ::EFI/BOOT
	mmd -i EFI/BOOT/efiboot.img ::EFI/enroll

	# copy grub image into disk image file
	for efi_file in $(find /boot/efi/EFI/BOOT/ -iname '*.efi' -and -not -iname 'fbx*'); do
		mcopy -v -i EFI/BOOT/efiboot.img "$efi_file" ::EFI/BOOT/"$(basename "$efi_file")"
	done
	mcopy -v -i EFI/BOOT/efiboot.img EFI/BOOT/grub.cfg          ::EFI/BOOT/grub.cfg
	mcopy -v -i EFI/BOOT/efiboot.img /etc/pki/uefi/altlinux.cer ::EFI/enroll/altlinux.cer
)

# output: $in_workdir/squashfs-files.txt
SUBSTEPS+=( prepare_workdir::create_squashfs_files_list )
prepare_workdir::create_squashfs_files_list() (
	readonly SQUASHFS_PACKAGES_LOG="${TMPDIR:-/tmp}/squashfs-packages-log.txt"

	cp -fv "$DATA_DIR/squashfs-files-default.txt" squashfs-files.txt
	rm -fv "$SQUASHFS_PACKAGES_LOG"
	while IFS="" read -r squashfs_package || [ -n "$squashfs_package" ]; do
		if [ -z "$(rpm -q "$squashfs_package" 2>>"$SQUASHFS_PACKAGES_LOG")" ]; then
			continue
		fi

		echo "Add the $squashfs_package to the altinst image" >> "$SQUASHFS_PACKAGES_LOG"
		rpm -ql "$squashfs_package" | while read -r squashfs_file; do
			# The file paths must be real, not through links, because
			# otherwise rsync will treat links as directories.
			if [ -L "$squashfs_file" ]; then
				dirname_link=$(dirname "$squashfs_file")
				realpath_to_link=$(realpath "$dirname_link")
				echo "$realpath_to_link/$(basename "$squashfs_file")" >> squashfs-files.txt
			else
				realpath "$squashfs_file" >> squashfs-files.txt
			fi
		done
	done < <(rpm -qa --qf "%{NAME}\n" | grep -E -x -f "$DATA_DIR/squashfs-packages-default.txt")

	find "/usr/lib/modules/$(uname -r)" >> squashfs-files.txt
)

SUBSTEPS+=( prepare_workdir::create_squashfs_altinst )
prepare_workdir::create_squashfs_altinst() (
	if [ ! -r squashfs-files.txt ]; then
		write_error 'No readable file squashfs-files.txt in workdir.'
		return 1
	fi

	rm   -rf squashfs-root
	mkdir -v squashfs-root
	expected_lines=$(rsync -avAXHW --dry-run --exclude="$in_workdir" \
		--files-from="$in_workdir/squashfs-files.txt" \
		/ "squashfs-root/" 2>/dev/null | wc -l)
	rsync -avAXHW \
		--exclude="$in_workdir" \
		--files-from="$in_workdir/squashfs-files.txt" \
		/ "squashfs-root/" | tee rsync-squashfs-root.log | expected_percentage_handler "$expected_lines" 30 50

	pushd squashfs-root || return 1
		rm -v usr/sbin/init
		if [ -f usr/sbin/install2-init ]; then
			mv -v usr/sbin/install2-init sbin/init
		else
			mv -v usr/libexec/install2/install2-init sbin/init
		fi

		# These folders should always be in the system, but let's be safe.
		# This is necessary for install2-init
		mkdir -pv sys mnt tmp proc
	popd || return 1

	chroot squashfs-root /usr/bin/bash -c "alternatives-validate; alternatives-update"

	rm -fv image/altinst
	mksquashfs squashfs-root image/altinst -no-recovery -comp xz -b 262144 -percentage | default_percentage_handler 50 100

	if ! test_bool "$in_lazy_cleanup"; then
		rm -rf squashfs-root
	fi
)



SUBSTEPS+=( prepare_workdir::create_install_scripts )
prepare_workdir::create_install_scripts() (
	rm -vf image/Metadata/install-scripts.tar
	rm -rf install-scripts

	mkdir -pv \
		install-scripts/postinstall.d \
		install-scripts/preinstall.d

	clear_scripts=( postinstall.d/00-remove-installer-pkgs.sh )
	clear_scripts+=( preinstall.d/00-cp-diskinfo.sh )
	clear_scripts+=( preinstall.d/05-copy-fstab )
	clear_scripts+=( preinstall.d/20-sysconfig.sh )

	if test_bool "$in_oem_mode"; then
		cp "$DATA_DIR/91-alterator-setup" install-scripts/postinstall.d/
		chmod +x install-scripts/postinstall.d/91-alterator-setup
	fi

	pushd install-scripts || return 1
	echo '#!/bin/sh' | tee ${clear_scripts[*]} > /dev/null
	chmod 755 ${clear_scripts[*]}
	popd || return 1

	mkdir -pv image/Metadata
	tar cf image/Metadata/install-scripts.tar -C install-scripts .

	if ! test_bool "$in_lazy_cleanup"; then
		rm -rf install-scripts
	fi
)

# Creating files that do not depend on the user's choice.
STEPS+=( prepare_workdir )
prepare_workdir() (
	mkdir -pv image/.disk
	rpm -qi alterator-kopidel > image/.disk/alterator-kopidel.txt

	mkdir -pv image/boot
	rm -vf image/boot/vmlinuz-*
	cp -v "/boot/vmlinuz-$(uname -r)" image/boot/

	prepare_workdir::create_install_scripts
	echo "step_progress: 3"
	prepare_workdir::create_initrd_image
	echo "step_progress: 20"
	prepare_workdir::create_grub_image
	echo "step_progress: 25"
	prepare_workdir::create_uefi_esp_image
	echo "step_progress: 27"
	prepare_workdir::create_squashfs_files_list
	echo "step_progress: 30"
	prepare_workdir::create_squashfs_altinst
	echo "step_progress: 100"
)

SUBSTEPS+=( create_copied_fs::cleanup_copied_fs )
create_copied_fs::cleanup_copied_fs() (
	find copied-fs/var/log -type f -delete
	find copied-fs/home/*/.bash_history -type f -delete
)

SUBSTEPS+=( create_copied_fs::oem_remove_human_users )
create_copied_fs::oem_remove_human_users() (
	if ! test_bool "$in_oem_mode"; then
		return
	fi

	cat copied-fs/etc/passwd | while read -r line; do
		username=$(echo "$line" | cut -d ':' -f 1)
		uid=$(     echo "$line" | cut -d ':' -f 3)

		if [ "$username" == "nobody" ]; then
			continue
		fi

		if [ "$uid" -ge "1000" ]; then
			chroot copied-fs /usr/bin/bash -c "userdel -rfRZ $username"
		fi
	done

	sed -i '/lastUser=.*/d' copied-fs/var/lib/ldm/.config/state-kde
)

update_ignored_files() (
	cat "$ALWAYS_IGNORED_FILES" > "$IGNORED_FILES"

	if   [ -z "$in_ignored_files" ]; then
		echo "A default list of ignored files is used"
		cat "$DATA_DIR/default-ignored-files.txt" >> "$IGNORED_FILES"
	elif [ -r "$in_ignored_files" ]; then
		echo "A custom list of ignored files is used: $in_ignored_files"
		cat "$in_ignored_files" >> "$IGNORED_FILES"
	elif [ -n "$in_ignored_files" ]; then
		write-error "There is no readable list of ignored files in the path: $in_ignored_files"
		return 1
	fi

	while read -r line; do
		fstype=$(echo "$line" | cut -d ' ' -f 1)
		target=$(echo "$line" | cut -d ' ' -f 2)

		if [[ "$fstype" == "nfs"* ]]; then
			echo "$target" >> "$IGNORED_FILES"
		fi
	done < <(findmnt -nro 'fstype,target')
)

STEPS+=( create_copied_fs )
create_copied_fs() (
	update_ignored_files || return 1

	rm    -rf copied-fs image/Metadata/copied-fs
	mkdir -pv copied-fs image/Metadata/copied-fs
	echo "$in_workdir" > image/Metadata/copied-fs/workdir-path.txt

	rsync_percentage=$( test_bool "$in_compress_copied" && echo 5 || echo 50 )
	expected_lines=$(rsync -avAXHW --dry-run \
		--exclude="$in_workdir" \
		--exclude="$WORKDIR_NAME" \
		--exclude-from="$IGNORED_FILES" \
		/ "copied-fs/" 2>/dev/null | wc -l)
	rsync -avAXHW \
		--exclude="$in_workdir" \
		--exclude="$WORKDIR_NAME" \
		--exclude-from="$IGNORED_FILES" \
		/ "copied-fs/" | tee rsync-copied-fs.log \
		| expected_percentage_handler "$expected_lines" 0 "$rsync_percentage"

	pushd "copied-fs" || return 1
	# These directories are necessary for mounting the chroot during installation.
	mkdir -pv -- \
		./dev \
		./proc \
		./sys \
		./run/lock/subsys \
		./run/alteratord \
		./tmp
	popd || return 1
	create_copied_fs::cleanup_copied_fs
	create_copied_fs::oem_remove_human_users

	tar_percentage=$( test_bool "$in_compress_copied" && echo 10 || echo 95 )
	tar cvf copied-fs.tar -C copied-fs . | expected_percentage_handler "$(find copied-fs | wc -l)" "$rsync_percentage" "$tar_percentage"

	if ! test_bool "$in_lazy_cleanup"; then
		rm -rf copied-fs
	fi

	if test_bool "$in_compress_copied"; then
		echo "step_progress: 10"
		{ pv -n copied-fs.tar | xz > copied-fs.tar.xz; } 2>&1 | default_percentage_handler 10 95
		rm -v copied-fs.tar
		mv copied-fs.tar.xz copied-fs.tar
	fi

	echo "step_progress: 95"

	split -b 3G copied-fs.tar "copied-fs.tar.part"
	mv -v copied-fs.tar.part* image/Metadata/copied-fs/
	rm -v copied-fs.tar
)

# This function makes parsing easier.
print_fstab_only_spaces() {
	sed -E -e 's/\t+/ /g' -e 's/ +/ /g' /etc/fstab
}

SUBSTEPS+=( create_disk_partition_info::subvolumes_info_by_uuid )
create_disk_partition_info::subvolumes_info_by_uuid() (
	UUID="${1:?}"
	result=

	result=$(print_fstab_only_spaces | grep "^UUID=$UUID .*[, ]subvol=/" |
		while read -r line; do
			name=$(echo "$line" | sed 's@.*subvol=/@@' | sed 's@,.*@@' | sed "s@ .*@@")
			mountpoint=$(echo "$line" | cut -d ' ' -f 2)

			if [[ -z "$name" || -z "$mountpoint" ]]; then
				continue
			fi
			echo -n " (\"$name\" . \"$mountpoint\")"
		done)

	if [ -n "$result" ]; then
		result="(subvols$result)"
	fi
	echo "$result"
)

# output: $in_workdir/image/Metadata/vm-profile.scm
STEPS+=( create_disk_partition_info )
create_disk_partition_info() (
	declare -A fsname_blkid_to_evms=( [ext4]="Ext4" [xfs]="XFS" [btrfs]="BtrFS" [vfat]="FAT32" [ntfs]="NTFS" [swap]="SWAPFS" )
	declare -A type_blkid_to_methods=( [part]="plain" [lvm]="lvm" )
	vm_profile=image/Metadata/vm-profile.scm

	rm -vf "$vm_profile"
	cp -v "$DATA_DIR/vm-profile-start.scm" "$vm_profile"

	lsblk -n --raw -b -o 'FSTYPE,SIZE,UUID,TYPE' | grep -E ' ((part)|(lvm))$' | sort --key 2 --numeric-sort | while read -r line; do
		FSTYPE=$(echo "$line" | cut -d ' ' -f 1)
		SIZE=$(  echo "$line" | cut -d ' ' -f 2)
		UUID=$(  echo "$line" | cut -d ' ' -f 3)
		TYPE=$(  echo "$line" | cut -d ' ' -f 4)

		if [[ -z "$FSTYPE" || -z "$SIZE" || -z "$UUID" ]]; then
			continue
		fi

		if [[ "$FSTYPE" == "LVM2_member" || "$FSTYPE" == "exfat" ]]; then
			continue
		fi

		max_size=$(( "$SIZE" / 512 ))
		min_size=$(( "$max_size" / 2 ))

		evms_FSTYPE=${fsname_blkid_to_evms["$FSTYPE"]}
		if [ -z "$evms_FSTYPE" ]; then
			write_error "Unknown FSTYPE: $FSTYPE"
			return 1
		fi
		methods=${type_blkid_to_methods["$TYPE"]}
		if [ -z "$methods" ]; then
			write_error "Unknown TYPE: $TYPE"
			return 1
		fi
		mountpoint=$(print_fstab_only_spaces | grep "^UUID=$UUID " | grep -v "[, ]subvol=/" | head -n 1 | cut -d ' ' -f 2)
		subvolumes=$(create_disk_partition_info::subvolumes_info_by_uuid "$UUID")
		if [[ -z "$mountpoint" && -z "$subvolumes" ]]; then
			echo "Mountpoint of partition with uuid $UUID not found in the /etc/fstab!"
			continue
		fi

		echo "               (\"$mountpoint\" (size $min_size . $max_size ) (fsim . \"$evms_FSTYPE\") (methods $methods) $subvolumes)" >> "$vm_profile"
	done

	echo '               )))' >> "$vm_profile"
)

STEPS+=( create_autoinstall_scm )
create_autoinstall_scm() (
	cp -vf "$DATA_DIR/autoinstall-start.scm" image/Metadata/autoinstall.scm
	if [[ "$BOOT_MODE" == legacy ]]; then
		echo '("/install-fs" action "legacy-install-grub")' >> image/Metadata/autoinstall.scm
	fi
	echo '("/preinstall" action "write")' >> image/Metadata/autoinstall.scm
	if [[ -n "$in_grub_device" && "$BOOT_MODE" == UEFI ]]; then
		echo '("/grub" action "write" device "'"$in_grub_device"'" passwd #f passwd_1 "*" passwd_2 "*")' >> image/Metadata/autoinstall.scm
	fi
)

STEPS+=( create_system_iso )
create_system_iso() (
	pushd "image" || return 1

	uuid_iso=$(find . -maxdepth 1 -regextype sed -regex "$UUID_REGEX" -printf "%f\n" | head -n 1 | sed 's/-//g')
	echo "uuid_iso: $uuid_iso"

	xorriso \
		-volume_date uuid "$uuid_iso" \
		-follow param \
		-read_mkisofsrc \
		-as mkisofs \
		-v -J -l -r -joliet-long \
		-eltorito-boot boot/grub/bios.img \
		-no-emul-boot \
		-boot-load-size 4 \
		-boot-info-table \
		-partition_offset 16 \
		--eltorito-catalog  boot/grub/boot.cat \
		--grub2-boot-info \
		--grub2-mbr "$GRUB_i386PC_LIBDIR/boot_hybrid.img" \
		--mbr-force-bootable \
		-eltorito-alt-boot \
		-e EFI/BOOT/efiboot.img \
		-no-emul-boot \
		-append_partition 2 0xef EFI/BOOT/efiboot.img \
		-appended_part_as_gpt \
		-o ../razlivochniy.iso \
		. \
		2>&1 >/dev/null | xorriso_percentage_handler 0 100
)
