#!/usr/bin/env bash
set -Eeuo pipefail
export LC_ALL=C

# Change this value or pass the version as the first argument.
VERSION="7.23.8"
WORKDIR=""
WRITE_STARTED=0

log() {
    printf '\n[INFO] %s\n' "$*"
}

die() {
    printf '\n[ERROR] %s\n' "$*" >&2
    exit 1
}

cleanup() {
    if [[ -n "$WORKDIR" && -d "$WORKDIR" ]]; then
        rm -rf -- "$WORKDIR"
    fi
}

on_error() {
    local rc="$1"
    local line="$2"
    trap - ERR

    printf '\n[ERROR] Failed at line %s (exit %s).\n' \
        "$line" "$rc" >&2

    if (( WRITE_STARTED )); then
        printf '%s\n' \
            'Disk writing has started; the disk may be partially overwritten.' \
            'No automatic reboot will be performed.' >&2
    else
        printf '%s\n' 'Installation stopped before disk writing.' >&2
    fi

    exit "$rc"
}

# Official CHR RAW image checksums, MikroTik download page, 2026-10-30.
select_image() {
    VERSION="${2:-7.23.8}"
    [[ "$VERSION" =~ ^7\.[0-9]+(\.[0-9]+)?$ ]] || die "Invalid RouterOS version."
    case "$1" in
        x86_64) ARCH=x86_64; MEMBER="chr-${VERSION}.img"; DEFAULT_SHA256=ac01ec7c092f4cab8136212e8ebfd613e0a37844c406317fa228d1ad446ce62c ;;
        aarch64|arm64) ARCH=arm64; MEMBER="chr-${VERSION}-arm64.img"; DEFAULT_SHA256=b197b11e388934967af0c2d65244fe3cf6c84e0cfa14ea725de495697cad979b ;;
        *) die "Unsupported architecture: $1. Use x86_64 or ARM64, not 32-bit ARM." ;;
    esac
    EXPECTED_SHA256="${3:-}"
    if [[ -z "$EXPECTED_SHA256" && "$VERSION" == 7.23.8 ]]; then EXPECTED_SHA256="$DEFAULT_SHA256"; fi
    [[ "$EXPECTED_SHA256" =~ ^[a-fA-F0-9]{64}$ ]] || die "Provide the official SHA256 for this version and architecture as argument 2."
}
verify_archive() {
    printf '%s  %s\n' "$2" "$1" | sha256sum --check --status || die "Official SHA256 mismatch. Disk will NOT be written."
}

main() {
trap cleanup EXIT
trap 'on_error "$?" "$LINENO"' ERR
trap 'exit 130' INT
trap 'exit 143' TERM

[[ $EUID -eq 0 ]] || die "Run this script as root."
select_image "$(uname -m)" "${1:-7.23.8}" "${2:-}"
if [[ "$ARCH" == "arm64" && ! -d /sys/firmware/efi ]]; then
    die "ARM64 requires UEFI firmware. Confirm boot support with your provider."
fi

[[ "$VERSION" =~ ^7\.[0-9]+(\.[0-9]+)?$ ]] ||
    die "Invalid version. Example: 7.12"

# Install download/extraction tools if missing.
if ! command -v curl >/dev/null 2>&1 ||
   ! command -v unzip >/dev/null 2>&1; then
    log "Installing curl, unzip and CA certificates"
    command -v apt-get >/dev/null 2>&1 || die "Install curl, unzip and CA certificates with your distribution package manager first."
    apt-get update
    apt-get install -y curl unzip ca-certificates
fi

for cmd in sha256sum lsblk findmnt readlink awk sort blockdev \
           dd sync stat df mktemp rm sleep; do
    command -v "$cmd" >/dev/null 2>&1 ||
        die "Required command is missing: $cmd"
done

[[ -w /proc/sysrq-trigger ]] ||
    die "Cannot access /proc/sysrq-trigger."

# Resolve the root filesystem to its backing block device.
ROOT_ID="$(findmnt -nro MAJ:MIN /)"
[[ -n "$ROOT_ID" && -e "/dev/block/$ROOT_ID" ]] ||
    die "Cannot resolve the root filesystem block device."

ROOT_DEVICE="$(readlink -f "/dev/block/$ROOT_ID")"

# Follow partitions/LVM/device-mapper back to physical/virtual disks.
DISK_TREE="$(lsblk -snrpo NAME,TYPE "$ROOT_DEVICE")"
mapfile -t DISKS < <(
    printf '%s\n' "$DISK_TREE" |
        awk '$2 == "disk" {print $1}' |
        sort -u
)

((${#DISKS[@]} == 1)) ||
    die "Root filesystem does not map to exactly one disk. Refusing to guess."

TARGET="${DISKS[0]}"
[[ -b "$TARGET" ]] || die "Invalid target disk: $TARGET"
[[ "$(blockdev --getro "$TARGET")" == "0" ]] ||
    die "Target disk is read-only."

log "Detected system disk: $TARGET"
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS "$TARGET"

# Keep both the ZIP and extracted image in RAM.
[[ "$(findmnt -nro FSTYPE -T /dev/shm)" == "tmpfs" ]] ||
    die "/dev/shm is not a RAM-backed tmpfs."

WORKDIR="$(mktemp -d /dev/shm/chr-install.XXXXXXXX)"
ARCHIVE="$WORKDIR/chr.img.zip"
IMAGE="$WORKDIR/chr.img"
URL="https://download.mikrotik.com/routeros/${VERSION}/${MEMBER}.zip"

log "Downloading CHR $VERSION into RAM"
curl \
    --fail --location --show-error \
    --proto '=https' --proto-redir '=https' \
    --connect-timeout 20 --max-time 1800 \
    --retry 3 --retry-delay 3 \
    --output "$ARCHIVE" \
    "$URL"

[[ -s "$ARCHIVE" ]] || die "Downloaded archive is empty."

verify_archive "$ARCHIVE" "$EXPECTED_SHA256"

log "Checking ZIP integrity"
unzip -tq "$ARCHIVE"

MEMBERS="$(unzip -Z1 "$ARCHIVE")"
[[ "$MEMBERS" == "$MEMBER" ]] ||
    die "Unexpected archive contents."

ZIP_LIST="$(unzip -l "$ARCHIVE")"
IMAGE_BYTES="$(
    printf '%s\n' "$ZIP_LIST" |
        awk -v name="$MEMBERS" '$4 == name {print $1}'
)"

[[ "$IMAGE_BYTES" =~ ^[0-9]+$ ]] ||
    die "Cannot determine image size."
(( IMAGE_BYTES > 0 )) || die "Image is empty."

AVAILABLE="$(df -B1 --output=avail "$WORKDIR" | awk 'NR == 2 {print $1}')"
(( AVAILABLE > IMAGE_BYTES + 16777216 )) ||
    die "Not enough free space in /dev/shm to extract the image."

DISK_BYTES="$(blockdev --getsize64 "$TARGET")"
(( DISK_BYTES >= IMAGE_BYTES )) ||
    die "Target disk is smaller than the image."

log "Extracting CHR into RAM"
unzip -p "$ARCHIVE" "$MEMBERS" > "$IMAGE"

[[ "$(stat -c %s "$IMAGE")" == "$IMAGE_BYTES" ]] ||
    die "Extracted image size mismatch."

rm -f -- "$ARCHIVE"

printf '\nVersion: %s\nTarget:  %s\n' "$VERSION" "$TARGET"
printf 'WARNING: The system disk will be overwritten and ALL its data removed.\n'
read -r -p "Type YES to install and reboot: " CONFIRM
[[ "$CONFIRM" == "YES" ]] || die "Cancelled."

# Original live-install sequence.
# Failure at any step stops the script without an automatic reboot.
log "Syncing filesystems"
sync

log "Requesting read-only remount"
printf 'u\n' > /proc/sysrq-trigger
sleep 3

log "Writing CHR image to $TARGET"
WRITE_STARTED=1
dd if="$IMAGE" of="$TARGET" bs=4M conv=fsync status=progress

log "Syncing disk"
sync
printf 's\n' > /proc/sysrq-trigger

log "Installation completed. Rebooting in 5 seconds"
sleep 5

printf 'b\n' > /proc/sysrq-trigger
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@"; fi
