#!/usr/bin/env bash
set -euo pipefail

base_url="${WAX_INSTALL_BASE_URL:-https://downloads.waxlang.dev}"
index_base_url="${WAX_INSTALL_INDEX_BASE_URL:-https://packages.waxlang.dev/v1/releases}"
install_root="${WAX_INSTALL_ROOT:-${HOME:-}/.wax}"
requested_version=""
target_override="${WAX_INSTALL_TARGET:-}"
allow_fixture="${WAX_INSTALL_ALLOW_INSECURE_FIXTURE:-0}"
no_path_update=0
no_file_association=0
uninstall=0
assume_yes=0
# Every diagnostic names the command the user actually ran.
label="install"
maximum_index_size=8192
maximum_archive_size=536870912
maximum_archive_entries=20000

valid_release_version() {
    local candidate="$1"
    local prerelease=""
    local identifiers=()
    local identifier=""
    if [[ ! "$candidate" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$ ]]; then
        return 1
    fi
    prerelease="${BASH_REMATCH[5]:-}"
    [[ -z "$prerelease" ]] && return 0
    IFS=. read -r -a identifiers <<< "$prerelease"
    for identifier in "${identifiers[@]}"; do
        if [[ "$identifier" =~ ^[0-9]+$ && ${#identifier} -gt 1 && "$identifier" == 0* ]]; then return 1; fi
    done
    return 0
}

usage() {
    echo "usage: install.sh [--version VERSION] [--install-root PATH] [--no-path-update] [--no-file-association]"
    echo "       install.sh --uninstall [--install-root PATH] [--yes]"
}

fail() {
    echo "wax $label: $1" >&2
    exit 1
}

note() {
    printf 'wax %s: %s\n' "$label" "$1" >&2
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --version)
            [[ $# -ge 2 ]] || { usage >&2; exit 2; }
            requested_version="$2"
            shift 2
            ;;
        --install-root)
            [[ $# -ge 2 ]] || { usage >&2; exit 2; }
            install_root="$2"
            shift 2
            ;;
        --no-path-update)
            no_path_update=1
            shift
            ;;
        --no-file-association)
            no_file_association=1
            shift
            ;;
        --uninstall)
            uninstall=1
            label="uninstall"
            shift
            ;;
        --yes)
            assume_yes=1
            shift
            ;;
        --help|-h)
            usage
            exit 0
            ;;
        *)
            usage >&2
            exit 2
            ;;
    esac
done

# --- PATH configuration ------------------------------------------------------

# Quote a path for insertion into a POSIX shell startup file without allowing
# spaces, quotes, or shell metacharacters in the path to change its meaning.
shell_quote() {
    printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
}

validate_path_configuration() {
    # A piped Bash process can receive only exported environment variables, so
    # custom zsh and fish config roots are intentionally read from the
    # inherited environment rather than guessed from an unrelated shell.
    case "${SHELL:-}" in
        */fish|fish)
            [[ "${XDG_CONFIG_HOME:-$HOME/.config}" == /* ]] || fail "XDG_CONFIG_HOME must be an absolute path"
            [[ ! "${XDG_CONFIG_HOME:-$HOME/.config}" =~ [[:cntrl:]] ]] || fail "XDG_CONFIG_HOME contains control characters"
            ;;
        */zsh|zsh)
            [[ "${ZDOTDIR:-$HOME}" == /* ]] || fail "ZDOTDIR must be an absolute path"
            [[ ! "${ZDOTDIR:-$HOME}" =~ [[:cntrl:]] ]] || fail "ZDOTDIR contains control characters"
            ;;
    esac
}

path_startup_files() {
    case "${SHELL:-}" in
        */bash|bash)
            # Bash reads .bashrc for interactive shells and the first existing
            # login profile for login shells. Keep both paths covered, using
            # the same guarded block so sourcing both does not duplicate PATH.
            printf '%s/.bashrc\n' "$HOME"
            if [[ -f "$HOME/.bash_profile" ]]; then
                printf '%s/.bash_profile\n' "$HOME"
            elif [[ -f "$HOME/.bash_login" ]]; then
                printf '%s/.bash_login\n' "$HOME"
            else
                printf '%s/.profile\n' "$HOME"
            fi
            ;;
        */fish|fish) printf '%s/fish/config.fish\n' "${XDG_CONFIG_HOME:-$HOME/.config}" ;;
        */zsh|zsh) printf '%s/.zshrc\n' "${ZDOTDIR:-$HOME}" ;;
        *) printf '%s/.profile\n' "$HOME" ;;
    esac
}

# One definition of the installed block text, written by the install and matched
# verbatim by the uninstall. The flavor is a parameter rather than a second read
# of $SHELL because the uninstall recognizes a block by the file that holds it.
path_block_text() {
    local flavor="$1"
    local path_entry="$2"
    local path_marker="$3"
    case "$flavor" in
        fish)
            # fish_add_path is itself persistent and duplicate-safe.
            printf '%s\nfish_add_path --global --path %s' "$path_marker" "$(shell_quote "$path_entry")"
            ;;
        *)
            # The guard is needed because Bash may source both .bash_profile
            # and .bashrc during one login, and because this block is installed
            # into both files when no single profile covers every Bash mode.
            printf '%s\ncase ":${PATH:-}:" in\n    *:%s:*) ;;\n    *) PATH=%s${PATH:+:$PATH} ;;\nesac\nexport PATH' "$path_marker" "$(shell_quote "$path_entry")" "$(shell_quote "$path_entry")"
            ;;
    esac
}

path_block_flavor_for_shell() {
    case "${SHELL:-}" in
        */fish|fish) printf 'fish\n' ;;
        *) printf 'posix\n' ;;
    esac
}

print_path_instructions() {
    local message="$1"
    local path_entry="$install_root/bin"
    local path_block="$(path_block_text "$(path_block_flavor_for_shell)" "$path_entry" "# Wax toolchain PATH: $path_entry")"
    echo "$message"
    echo "add $path_entry to PATH so wax, waxc, waxdbg, and waxlsp are available with:"
    printf '%s\n' "$path_block"
}

configure_path() {
    local path_entry="$install_root/bin"
    if [[ "$no_path_update" == 1 ]]; then
        echo "add $path_entry to PATH"
        return
    fi
    local startup_files=()
    local startup_file=""
    local primary_startup_file=""
    local path_marker="# Wax toolchain PATH: $path_entry"
    local path_block="$(path_block_text "$(path_block_flavor_for_shell)" "$path_entry" "$path_marker")"
    while IFS= read -r startup_file; do
        startup_files+=("$startup_file")
    done < <(path_startup_files)
    primary_startup_file="${startup_files[0]}"
    local reload_command=". $(shell_quote "$primary_startup_file")"
    local answer=""
    case ":${PATH:-}:" in
        *":$path_entry:"*)
            echo "PATH already includes $path_entry"
            return
            ;;
    esac
    # curl | bash consumes stdin for the script, so an interactive answer must
    # come from the controlling terminal instead of the script's stdin. A
    # readable /dev/tty is the whole condition: stdout is often redirected into
    # a log by someone sitting right there, and treating that as unattended
    # would edit their startup files without asking. Where there really is
    # nobody to ask, leaving a freshly installed toolchain off PATH is not a
    # useful default -- --no-path-update is the opt-out for installs that
    # manage PATH themselves. The device node can exist and be readable in a
    # process that has no controlling terminal, where opening it fails with
    # ENXIO, so the probe opens it rather than testing the path.
    if { : < /dev/tty; } 2>/dev/null && { : > /dev/tty; } 2>/dev/null; then
        printf 'Add %s to your shell startup files so wax, waxc, waxdbg, and waxlsp are available? [Y/n] ' "$path_entry" > /dev/tty
        if ! IFS= read -r answer < /dev/tty; then
            print_path_instructions "PATH was not changed (no answer received)"
            return
        fi
        case "$answer" in
            ""|[yY]|[yY][eE][sS])
                ;;
            [nN]|[nN][oO])
                print_path_instructions "PATH was not changed"
                return
                ;;
            *)
                print_path_instructions "PATH was not changed (answer must be yes or no)"
                return
                ;;
        esac
    fi
    for startup_file in "${startup_files[@]}"; do
        if [[ -e "$startup_file" && ! -f "$startup_file" ]]; then
            echo "could not update PATH: $startup_file is not a regular file" >&2
            print_path_instructions "add $path_entry to PATH with this block:"
            return
        fi
    done
    if [[ "${SHELL:-}" == */fish || "${SHELL:-}" == fish ]]; then mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/fish"; fi
    if [[ "${SHELL:-}" == */zsh || "${SHELL:-}" == zsh ]]; then mkdir -p "${ZDOTDIR:-$HOME}"; fi
    for startup_file in "${startup_files[@]}"; do
        if [[ -f "$startup_file" ]] && grep -Fqx "$path_marker" "$startup_file"; then
            continue
        fi
        # An unwritable startup file (a read-only home, a root-owned profile in a
        # container) must not abort an otherwise complete install under set -e.
        if ! { [[ ! -s "$startup_file" ]] || printf '\n' >> "$startup_file"; } || ! printf '%s\n' "$path_block" >> "$startup_file"; then
            print_path_instructions "could not update PATH: $startup_file is not writable"
            return
        fi
        echo "added $path_entry to PATH in $startup_file"
    done
    echo "open a new shell or run: $reload_command"
}

# Quote one argument for the Desktop Entry Exec grammar. Percent is handled
# separately because it introduces field codes even inside a quoted argument.
desktop_exec_quote() {
    local value="$1"
    value="${value//\\/\\\\\\\\}"
    value="${value//\"/\\\"}"
    value="${value//\$/\\$}"
    value="${value//\`/\\\`}"
    value="${value//%/%%}"
    printf '"%s"' "$value"
}

install_recording_file_type() {
    local data_home="$1"
    local mime_directory="$data_home/mime"
    local icon_directory="$data_home/icons/hicolor/256x256/mimetypes"
    local application_directory="$data_home/applications"
    local mime_source="$install_root/current/share/mime/wax-recording.xml"
    local icon_source="$install_root/current/share/icons/wax-recording.png"
    local mime_staging=""
    local icon_staging=""
    local desktop_staging=""
    local mime_destination="$mime_directory/packages/wax-recording.xml"
    local icon_destination="$icon_directory/application-x-wax-recording.png"
    local desktop_destination="$application_directory/dev.waxlang.WaxRecording.desktop"
    local waxdbg_exec=""
    [[ -f "$mime_source" && -f "$icon_source" ]] || return 1
    mkdir -p "$mime_directory/packages" "$icon_directory" "$application_directory" || return 1
    [[ ! -d "$mime_destination" && ! -d "$icon_destination" && ! -d "$desktop_destination" ]] || return 1
    mime_staging="$(mktemp "$mime_directory/packages/.wax-recording.xml.XXXXXX")" || return 1
    if ! icon_staging="$(mktemp "$icon_directory/.application-x-wax-recording.png.XXXXXX")"; then
        rm -f "$mime_staging"
        return 1
    fi
    if ! desktop_staging="$(mktemp "$application_directory/.dev.waxlang.WaxRecording.desktop.XXXXXX")"; then
        rm -f "$mime_staging" "$icon_staging"
        return 1
    fi
    waxdbg_exec="$(desktop_exec_quote "$install_root/bin/waxdbg")"
    if ! {
        cp "$mime_source" "$mime_staging" &&
        cp "$icon_source" "$icon_staging" &&
        printf '%s\n' \
            '[Desktop Entry]' \
            'Type=Application' \
            'Name=Wax Inspector' \
            'Comment=Open a Wax recording' \
            "Exec=$waxdbg_exec gui %f" \
            'Icon=application-x-wax-recording' \
            'Terminal=false' \
            'NoDisplay=true' \
            'Categories=Development;' \
            'MimeType=application/x-wax-recording;' > "$desktop_staging" &&
        chmod 0644 "$mime_staging" "$icon_staging" "$desktop_staging" &&
        mv -f "$mime_staging" "$mime_destination" &&
        mv -f "$icon_staging" "$icon_destination" &&
        mv -f "$desktop_staging" "$desktop_destination"
    }; then
        rm -f "$mime_staging" "$icon_staging" "$desktop_staging"
        return 1
    fi
    if command -v update-mime-database >/dev/null 2>&1; then
        update-mime-database "$mime_directory" >/dev/null 2>&1 || note "could not refresh the desktop MIME cache; it will refresh at the next desktop login"
    fi
    if command -v update-desktop-database >/dev/null 2>&1; then
        update-desktop-database "$application_directory" >/dev/null 2>&1 || note "could not refresh the desktop application cache; it will refresh at the next desktop login"
    fi
    return 0
}

configure_recording_file_type() {
    [[ "$target" == linux-* && "$no_file_association" == 0 ]] || return 0
    local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
    if [[ "$data_home" != /* || "$data_home" =~ [[:cntrl:]] ]]; then
        note "did not install the .wxs file association because XDG_DATA_HOME is not a safe absolute path"
        return
    fi
    if install_recording_file_type "$data_home"; then
        echo "registered .wxs files with Wax Inspector"
    else
        note "could not install the optional .wxs desktop integration"
    fi
}

# --- Uninstall ---------------------------------------------------------------

# Candidate startup files across every shell the installer writes to, not just
# the shell running now: a toolchain installed from one shell must be removable
# from another.
uninstall_startup_files() {
    printf '%s/.bashrc\n%s/.bash_profile\n%s/.bash_login\n%s/.profile\n' "$HOME" "$HOME" "$HOME" "$HOME"
    local zsh_home="${ZDOTDIR:-$HOME}"
    if [[ "$zsh_home" == /* && ! "$zsh_home" =~ [[:cntrl:]] ]]; then printf '%s/.zshrc\n' "$zsh_home"; fi
    local config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
    if [[ "$config_home" == /* && ! "$config_home" =~ [[:cntrl:]] ]]; then printf '%s/fish/config.fish\n' "$config_home"; fi
}

# Which flavor a startup file holds, read from the file rather than from $SHELL,
# so a fish block is recognized when uninstalling from another shell.
path_block_flavor_for_file() {
    case "$1" in
        */fish/config.fish) printf 'fish\n' ;;
        *) printf 'posix\n' ;;
    esac
}

# Remove the installed block only when the file still holds it verbatim. An
# edited block is left alone and reported: guessing at what a user changed is
# how an uninstaller corrupts a startup file.
remove_path_block() {
    local startup_file="$1"
    local path_entry="$2"
    local path_marker="$3"
    local path_block="$4"
    local staging=""
    [[ -f "$startup_file" ]] || return 0
    grep -Fqx "$path_marker" "$startup_file" || return 0
    if ! staging="$(mktemp "$(dirname "$startup_file")/.wax-uninstall.XXXXXX")"; then
        note "could not rewrite $startup_file; remove its PATH block by hand"
        return 0
    fi
    if ! WAX_PATH_BLOCK="$path_block" awk '
        BEGIN { expected_count = split(ENVIRON["WAX_PATH_BLOCK"], expected, "\n") }
        { line[NR] = $0 }
        END {
            for (i = 1; i <= NR && !removed; i++) {
                if (line[i] != expected[1]) continue
                matched = 1
                for (j = 2; j <= expected_count; j++) if (line[i + j - 1] != expected[j]) { matched = 0; break }
                if (!matched) continue
                start = (i > 1 && line[i - 1] == "") ? i - 1 : i
                for (j = start; j < i + expected_count; j++) skip[j] = 1
                removed = 1
            }
            for (i = 1; i <= NR; i++) if (!(i in skip)) print line[i]
            exit removed ? 0 : 1
        }
    ' "$startup_file" > "$staging"; then
        rm -f "$staging"
        note "left the PATH block in $startup_file in place because it no longer matches what the installer wrote; remove it by hand"
        return 0
    fi
    # Write through the path rather than replacing it: a startup file is often a
    # symbolic link into a dotfiles repository, and moving over the link would
    # leave a plain copy behind while the real file kept the block.
    if ! cat "$staging" > "$startup_file"; then
        rm -f "$staging"
        note "could not rewrite $startup_file; remove its PATH block by hand"
        return 0
    fi
    rm -f "$staging"
    echo "removed $path_entry from PATH in $startup_file"
}

remove_recording_file_type() {
    local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
    [[ "$data_home" == /* && ! "$data_home" =~ [[:cntrl:]] ]] || return 0
    local mime_directory="$data_home/mime"
    local application_directory="$data_home/applications"
    local desktop_file="$application_directory/dev.waxlang.WaxRecording.desktop"
    # The whole registration -- handler, MIME declaration, and icon -- belongs to
    # whichever install root the desktop entry launches, and the entry is the
    # only thing that names one. Without it nothing here is ours to delete.
    [[ -f "$desktop_file" ]] || return 0
    grep -Fqx "Exec=$(desktop_exec_quote "$install_root/bin/waxdbg") gui %f" "$desktop_file" || {
        note "left the .wxs file association in place because it points at another Wax installation"
        return 0
    }
    rm -f "$desktop_file" "$mime_directory/packages/wax-recording.xml" "$data_home/icons/hicolor/256x256/mimetypes/application-x-wax-recording.png"
    if command -v update-mime-database >/dev/null 2>&1; then
        update-mime-database "$mime_directory" >/dev/null 2>&1 || note "could not refresh the desktop MIME cache; it will refresh at the next desktop login"
    fi
    if command -v update-desktop-database >/dev/null 2>&1; then
        update-desktop-database "$application_directory" >/dev/null 2>&1 || note "could not refresh the desktop application cache; it will refresh at the next desktop login"
    fi
    echo "removed the .wxs file association"
}

run_uninstall() {
    local path_entry="$install_root/bin"
    local path_marker="# Wax toolchain PATH: $path_entry"
    local startup_file=""
    local answer=""
    local lock_pid=""
    [[ "$install_root" != "$HOME" && "$install_root" != "/" ]] || fail "refusing to remove $install_root"
    [[ -d "$install_root/releases" ]] || fail "$install_root is not an installer-managed Wax install root"
    if [[ -f "$install_root/.install-lock/pid" ]]; then
        IFS= read -r lock_pid < "$install_root/.install-lock/pid" || true
        if [[ "$lock_pid" =~ ^[1-9][0-9]*$ ]] && kill -0 "$lock_pid" 2>/dev/null; then
            fail "a Wax installation is running (process $lock_pid); wait for it to finish"
        fi
    fi
    # A controlling terminal is enough to ask, where the install also requires a
    # tty on stdout: `wax uninstall > log` must still be answered by a person.
    # Without one there is nobody to answer, so this refuses rather than
    # destroying an installation unasked; --yes is the way to say it up front.
    # The subshell is the test, because a readable /dev/tty can still fail to
    # open in a session that has no controlling terminal.
    if [[ "$assume_yes" != 1 ]]; then
        (exec 3>/dev/tty) 2>/dev/null || fail "no terminal to confirm at; rerun with --yes to remove $install_root"
        printf 'Remove the Wax toolchain in %s, its PATH entry, and its file associations? [y/N] ' "$install_root" > /dev/tty
        IFS= read -r answer < /dev/tty || answer=""
        case "$answer" in
            [yY]|[yY][eE][sS]) ;;
            *) fail "cancelled" ;;
        esac
    fi
    while IFS= read -r startup_file; do
        remove_path_block "$startup_file" "$path_entry" "$path_marker" "$(path_block_text "$(path_block_flavor_for_file "$startup_file")" "$path_entry" "$path_marker")"
    done < <(uninstall_startup_files)
    remove_recording_file_type
    # Last, because this deletes the script that is running. Bash keeps reading
    # from its open descriptor, and the remaining work is output.
    rm -rf "$install_root" || fail "could not remove $install_root; its PATH entry and file associations are already gone, so delete the directory by hand"
    echo "removed $install_root"
    echo "open a new shell so PATH no longer includes $path_entry"
    echo "the editor extension installed by 'wax vscode' was left in place; remove it from your editor"
}

if [[ -n "$requested_version" ]] && ! valid_release_version "$requested_version"; then
    fail "invalid release version: $requested_version"
fi
[[ -n "$install_root" && "$install_root" == /* ]] || fail "install root must be an absolute path"
[[ ! "$install_root" =~ [[:cntrl:]:] ]] || fail "install root must not contain control characters or ':'"
[[ -n "${HOME:-}" && "${HOME:-}" == /* ]] || fail "HOME must be an absolute path"
[[ ! "${HOME:-}" =~ [[:cntrl:]] ]] || fail "HOME contains control characters"

if [[ "$uninstall" == 1 ]]; then
    run_uninstall
    exit 0
fi

base_url="${base_url%/}"
index_base_url="${index_base_url%/}"
if [[ "$allow_fixture" == 1 ]]; then
    [[ "$base_url" =~ ^http://(127\.0\.0\.1|localhost):[1-9][0-9]{0,4}$ ]] || fail "fixture downloads require an HTTP loopback URL"
    [[ "$index_base_url" =~ ^http://(127\.0\.0\.1|localhost):[1-9][0-9]{0,4}(/[A-Za-z0-9._~-]+)*$ ]] || fail "fixture indexes require an HTTP loopback URL"
else
    [[ "$base_url" =~ ^https://[A-Za-z0-9][A-Za-z0-9.-]*(/[A-Za-z0-9._~-]+)*$ ]] || fail "download base URL must be HTTPS without credentials, a port, query, or fragment"
    [[ "$index_base_url" =~ ^https://[A-Za-z0-9][A-Za-z0-9.-]*(/[A-Za-z0-9._~-]+)*$ ]] || fail "index base URL must be HTTPS without credentials, a port, query, or fragment"
fi
[[ "$base_url" != *"/../"* && "$base_url" != *"/./"* && "$base_url" != */.. && "$base_url" != */. ]] || fail "download base URL contains an unsafe path"
[[ "$index_base_url" != *"/../"* && "$index_base_url" != *"/./"* && "$index_base_url" != */.. && "$index_base_url" != */. ]] || fail "index base URL contains an unsafe path"
if [[ "$no_path_update" == 0 ]]; then
    validate_path_configuration
fi

if [[ -n "$target_override" ]]; then
    target="$target_override"
else
    system="$(uname -s)"
    machine="$(uname -m)"
    if [[ "$system" == Darwin && "$machine" == arm64 ]]; then
        target="macos-arm64"
    elif [[ "$system" == Linux && "$machine" == x86_64 ]]; then
        target="linux-x64"
    elif [[ "$system" == Linux && ( "$machine" == aarch64 || "$machine" == arm64 ) ]]; then
        target="linux-arm64"
    else
        fail "unsupported platform: $system $machine"
    fi
fi
[[ "$target" == macos-arm64 || "$target" == linux-arm64 || "$target" == linux-x64 ]] || fail "unsupported release target: $target"

for command in curl tar mktemp wc grep sed find readlink diff uname tr mkdir rmdir rm mv ln sort cp chmod; do
    command -v "$command" >/dev/null 2>&1 || fail "required command is unavailable: $command"
done
if command -v shasum >/dev/null 2>&1; then
    sha256_file() {
        shasum -a 256 "$1" | sed 's/^\\//; s/[[:space:]].*$//'
    }
elif command -v sha256sum >/dev/null 2>&1; then
    sha256_file() {
        sha256sum "$1" | sed 's/^\\//; s/[[:space:]].*$//'
    }
else
    fail "SHA-256 tool is unavailable (expected shasum or sha256sum)"
fi

version_less_than() {
    local left="$1"
    local right="$2"
    local left_parts=()
    local right_parts=()
    local index
    IFS=. read -r -a left_parts <<< "$left"
    IFS=. read -r -a right_parts <<< "$right"
    for index in 0 1 2; do
        if [[ ${#left_parts[$index]} -lt ${#right_parts[$index]} ]]; then return 0; fi
        if [[ ${#left_parts[$index]} -gt ${#right_parts[$index]} ]]; then return 1; fi
        if [[ ${left_parts[$index]} < ${right_parts[$index]} ]]; then return 0; fi
        if [[ ${left_parts[$index]} > ${right_parts[$index]} ]]; then return 1; fi
    done
    return 1
}

managed_release_pattern='^releases/(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?-(macos-arm64|linux-arm64|linux-x64)-[0-9a-f]{16}$'
validate_release_link() {
    local name="$1"
    local link="$install_root/$name"
    local destination=""
    if [[ -L "$link" ]]; then
        destination="$(readlink "$link")"
        [[ "$destination" =~ $managed_release_pattern ]] || fail "$link is not a Wax-managed release link"
    elif [[ -e "$link" ]]; then
        fail "$link exists and is not a Wax-managed symbolic link"
    fi
}

mkdir -p "$install_root/releases" "$install_root/.release-integrity"
validate_release_link current
validate_release_link previous
if [[ -L "$install_root/bin" ]]; then
    [[ "$(readlink "$install_root/bin")" == current/bin ]] || fail "$install_root/bin is not a Wax-managed symbolic link"
elif [[ -e "$install_root/bin" ]]; then
    fail "$install_root/bin exists and is not a Wax-managed symbolic link"
fi

lock="$install_root/.install-lock"
if ! mkdir "$lock" 2>/dev/null; then
    lock_pid=""
    if [[ -f "$lock/pid" ]]; then
        IFS= read -r lock_pid < "$lock/pid" || true
    fi
    if [[ "$lock_pid" =~ ^[1-9][0-9]*$ ]] && kill -0 "$lock_pid" 2>/dev/null; then
        fail "another Wax installation is already running (process $lock_pid)"
    fi
    rm -rf "$lock"
    mkdir "$lock" 2>/dev/null || fail "another Wax installation acquired the install lock"
fi
echo "$$" > "$lock/pid"
staging=""
stable_version_staging=""
cleanup() {
    [[ -z "$staging" ]] || rm -rf "$staging"
    [[ -z "$stable_version_staging" ]] || rm -f "$stable_version_staging"
    rm -rf "$lock"
}
trap cleanup EXIT
staging="$(mktemp -d "$install_root/.staging.XXXXXX")"

download() {
    local url="$1"
    local output="$2"
    local maximum_size="$3"
    local requested_release="${4:-}"
    local arguments=(--fail --silent --show-error --location --max-redirs 0 --connect-timeout 15 --max-time 900 --max-filesize "$maximum_size" --retry 3 --output "$output")
    if [[ "$allow_fixture" != 1 ]]; then
        arguments+=(--proto "=https" --proto-redir "=https" --tlsv1.2)
    fi
    if ! curl --disable "${arguments[@]}" "$url"; then
        if [[ -n "$requested_release" ]]; then
            fail "requested Wax version $requested_release is unavailable (release index request failed)"
        fi
        fail "download failed: $url"
    fi
}

replace_link() {
    local source="$1"
    local destination="$2"
    if [[ "$(uname -s)" == Darwin ]]; then
        mv -fh "$source" "$destination"
    else
        mv -Tf "$source" "$destination"
    fi
}

# Fingerprint the installed tree by sorted path, contents, size, and executable
# bit. Ownership, timestamps, and other extraction metadata are intentionally
# excluded so the same verified release has one fingerprint on every host.
# The record lives outside the immutable release directory so an older install
# without one safely falls back to downloading and comparing the archive once.
release_fingerprint() {
    local directory="$1"
    local listing="$2"
    local file=""
    local relative=""
    local executable=0
    [[ -d "$directory" ]] || return 1
    : > "$listing"
    while IFS= read -r file; do
        relative="${file#"$directory"/}"
        executable=0
        [[ -x "$file" ]] && executable=1
        printf '%s|%s|%s|%s\n' "$(sha256_file "$file")" "$(wc -c < "$file" | tr -d ' ')" "$executable" "$relative" >> "$listing"
    done < <(find "$directory" -type f -print | LC_ALL=C sort)
    sha256_file "$listing"
}

if [[ -n "$requested_version" ]]; then
    index_url="$index_base_url/v$requested_version/index-v1.txt"
else
    index_url="$index_base_url/stable/index-v1.txt"
fi
index_file="$staging/index-v1.txt"
download "$index_url" "$index_file" "$maximum_index_size" "$requested_version"
index_size="$(wc -c < "$index_file" | tr -d ' ')"
[[ "$index_size" =~ ^[0-9]+$ && "$index_size" -gt 0 && "$index_size" -le "$maximum_index_size" ]] || fail "release index exceeds its size limit"
if LC_ALL=C grep -q '[^ -~]' "$index_file"; then
    fail "release index contains non-portable text"
fi
index_lines="$(wc -l < "$index_file" | tr -d ' ')"
[[ "$index_lines" =~ ^[4-8]$ ]] || fail "release index has an invalid line count"
[[ "$(sed -n '1p' "$index_file")" == wax-release-index-v1 ]] || fail "release index has an invalid format"
version_line="$(sed -n '2p' "$index_file")"
commit_line="$(sed -n '3p' "$index_file")"
version="${version_line#version=}"
commit="${commit_line#commit=}"
[[ "$version_line" == "version=$version" ]] && valid_release_version "$version" || fail "release index has an invalid version"
[[ "$commit_line" == "commit=$commit" && "$commit" =~ ^[0-9a-f]{40}$ ]] || fail "release index has an invalid commit"
[[ -z "$requested_version" || "$version" == "$requested_version" ]] || fail "release index returned version $version instead of $requested_version"
if [[ -z "$requested_version" ]]; then
    [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || fail "stable channel returned a prerelease"
    stable_version_file="$install_root/.stable-version"
    if [[ -f "$stable_version_file" ]]; then
        IFS= read -r installed_stable_version < "$stable_version_file" || fail "could not read installed stable version"
        [[ "$installed_stable_version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || fail "installed stable version state is invalid"
        version_less_than "$version" "$installed_stable_version" && fail "stable channel would downgrade from $installed_stable_version to $version"
    fi
fi

selected_size=""
selected_sha256=""
selected_root=""
selected_url=""
# One record per archive the release produced, in strictly ascending target
# order. Records for targets this installer cannot install — the Windows ones,
# which install.ps1 serves — are still validated, so a malformed index is
# rejected rather than skipped past.
supported_targets=(linux-arm64 linux-x64 macos-arm64 windows-arm64 windows-x64)
previous_rank=-1
for offset in $(seq 0 $((index_lines - 4))); do
    line="$(sed -n "$((offset + 4))p" "$index_file")"
    IFS='|' read -r target_field size sha256 root url extra <<< "$line"
    record_target="${target_field#target=}"
    [[ "$target_field" == "target=$record_target" && -z "${extra:-}" ]] || fail "release index has an invalid target record"
    rank=-1
    for candidate in "${!supported_targets[@]}"; do
        [[ "${supported_targets[$candidate]}" == "$record_target" ]] && rank="$candidate"
    done
    [[ "$rank" -ge 0 ]] || fail "release index has an unsupported target: $record_target"
    [[ "$rank" -gt "$previous_rank" ]] || fail "release index has out-of-order target records"
    previous_rank="$rank"
    [[ "$size" =~ ^[1-9][0-9]{0,8}$ && "$size" -le "$maximum_archive_size" ]] || fail "release index has an invalid $record_target size"
    [[ "$sha256" =~ ^[0-9a-f]{64}$ ]] || fail "release index has an invalid $record_target SHA-256"
    [[ "$root" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]] || fail "release index has an invalid $record_target archive root"
    expected_url="$base_url/releases/v$version/$root.tar.gz"
    [[ "$url" == "$expected_url" ]] || fail "release index has an invalid $record_target URL"
    if [[ "$record_target" == "$target" ]]; then
        selected_size="$size"
        selected_sha256="$sha256"
        selected_root="$root"
        selected_url="$url"
    fi
done
[[ -n "$selected_url" ]] || fail "release index does not contain $target"

release_name="$version-$target-${selected_sha256:0:16}"
release_directory="$install_root/releases/$release_name"
integrity_file="$install_root/.release-integrity/$release_name"
expected_current="releases/$release_name"
installed_current="$(readlink "$install_root/current" 2>/dev/null || true)"
archive_mebibytes=$(( (selected_size + 1048575) / 1048576 ))
note "Wax $version for $target uses a $archive_mebibytes MiB archive"
# The release directory name includes the archive digest, so an exact current
# target has already passed this installer's archive verification. Re-check the
# cheap structural invariants before avoiding a second full archive download.
# If anything looks wrong, continue through the normal verified extraction path,
# which either repairs from the published archive or reports the mismatch.
if [[ -d "$release_directory" ]]; then
    note "verifying the existing installation"
fi
installed_fingerprint="$(release_fingerprint "$release_directory" "$staging/installed-tree" 2>/dev/null || true)"
recorded_fingerprint="$(sed -n '1p' "$integrity_file" 2>/dev/null || true)"
integrity_extra="$(sed -n '2p' "$integrity_file" 2>/dev/null || true)"
if [[ "$installed_current" == "$expected_current" && -d "$release_directory/bin" && -f "$release_directory/release.json" && -f "$integrity_file" && ! -L "$integrity_file" && "$installed_fingerprint" == "$recorded_fingerprint" && -z "$integrity_extra" && -z "$(find "$release_directory" -type l -print -quit)" && -z "$(find "$release_directory/bin" -type f ! -perm -111 -print -quit)" ]] &&
   grep -Fqx "  \"version\": \"$version\"" "$release_directory/release.json" &&
   grep -Fqx "  \"target\": \"$target\"," "$release_directory/release.json" &&
   grep -Fqx "  \"commit\": \"$commit\"," "$release_directory/release.json"; then
    if [[ -z "$requested_version" ]]; then
        stable_version_staging="$install_root/.stable-version.$$"
        printf '%s\n' "$version" > "$stable_version_staging"
        mv -f "$stable_version_staging" "$install_root/.stable-version"
        stable_version_staging=""
    fi
    if [[ ! -L "$install_root/bin" ]]; then
        bin_link="$install_root/.bin.$$"
        ln -s current/bin "$bin_link"
        replace_link "$bin_link" "$install_root/bin"
    fi
    echo "Wax $version for $target is already up to date"
    configure_recording_file_type
    configure_path
    echo "run 'wax vscode' to install the editor extension"
    exit 0
fi

archive="$staging/$selected_root.tar.gz"
note "downloading the release archive"
download "$selected_url" "$archive" "$maximum_archive_size"
note "verifying the downloaded archive"
actual_size="$(wc -c < "$archive" | tr -d ' ')"
[[ "$actual_size" == "$selected_size" ]] || fail "release archive size mismatch"
[[ "$(sha256_file "$archive")" == "$selected_sha256" ]] || fail "release archive SHA-256 mismatch"

listing="$staging/archive.list"
tar -tzf "$archive" > "$listing"
[[ -s "$listing" ]] || fail "release archive is empty"
entry_count="$(wc -l < "$listing" | tr -d ' ')"
[[ "$entry_count" =~ ^[1-9][0-9]*$ && "$entry_count" -le "$maximum_archive_entries" ]] || fail "release archive contains too many entries"
if LC_ALL=C grep -q '[^ -~]' "$listing"; then
    fail "release archive contains a non-portable path"
fi
# An unsigned macOS candidate is named wax-<version>-macos-arm64.unsigned.tar.gz
# so that the signing stage can tell the two apart by file name, while the
# directory inside it stays wax-<version>-macos-arm64 -- signing rewrites the
# binaries in place and must not rename the bundle. The index's root field is the
# URL's basename, so it carries the suffix and the archive's own root does not.
bundle="${selected_root%.unsigned}"
while IFS= read -r entry; do
    [[ "$entry" == "$bundle/"* && "$entry" != *\\* && "$entry" != /* && "$entry" != *"/../"* && "$entry" != *"/./"* && "$entry" != */.. && "$entry" != */. ]] || fail "release archive contains an unsafe path"
done < "$listing"
verbose_listing="$staging/archive.verbose.list"
tar -tvzf "$archive" > "$verbose_listing"
while IFS= read -r entry; do
    [[ "${entry:0:1}" == - || "${entry:0:1}" == d ]] || fail "release archive contains an unsupported entry type"
done < "$verbose_listing"

extract="$staging/extract"
mkdir "$extract"
note "extracting the release archive"
tar -xzf "$archive" -C "$extract" --no-same-owner --no-same-permissions
[[ -d "$extract/$bundle" && -z "$(find "$extract" -mindepth 1 -maxdepth 1 ! -name "$bundle" -print -quit)" ]] || fail "release archive has an invalid root"
[[ -z "$(find "$extract/$bundle" -type l -print -quit)" ]] || fail "release archive contains a symbolic link"
manifest="$extract/$bundle/release.json"
[[ -f "$manifest" ]] || fail "release archive has no release.json"
[[ -d "$extract/$bundle/bin" ]] || fail "release archive has no bin directory"
grep -Fqx "  \"version\": \"$version\"" "$manifest" || fail "release manifest version does not match"
grep -Fqx "  \"target\": \"$target\"," "$manifest" || fail "release manifest target does not match"
grep -Fqx "  \"commit\": \"$commit\"," "$manifest" || fail "release manifest commit does not match"
# macOS builds are not yet Apple-signed or notarized. That is invisible on this
# path -- curl sets no quarantine attribute and arm64 binaries carry the linker's
# ad-hoc signature -- but it is visible to anyone who downloads the archive in a
# browser and lets Finder unpack it, so say it once rather than never. Every
# other target must be a canonical signed release, and macOS will be too once the
# Developer ID exists.
if grep -Fqx '  "state": "unsigned",' "$manifest"; then
    [[ "$target" == macos-arm64 ]] || fail "release archive is not a canonical signed release"
    printf 'wax install: this macOS build is not Apple-notarized; install it with this installer rather than by unpacking the archive in Finder\n' >&2
else
    grep -Fqx '  "state": "release",' "$manifest" || fail "release archive is not a canonical signed release"
fi
if [[ -e "$release_directory" ]]; then
    [[ -f "$release_directory/release.json" ]] || fail "existing release directory is invalid: $release_directory"
    [[ -z "$(find "$release_directory" -type l -print -quit)" ]] || fail "existing release directory contains a symbolic link: $release_directory"
    diff -qr "$extract/$bundle" "$release_directory" >/dev/null || fail "existing release directory does not match the verified archive: $release_directory"
    while IFS= read -r expected_path; do
        relative_path="${expected_path#"$extract/$bundle"}"
        installed_path="$release_directory$relative_path"
        if [[ -x "$expected_path" ]]; then
            [[ -x "$installed_path" ]] || fail "existing release directory has incorrect executable permissions: $installed_path"
        else
            [[ ! -x "$installed_path" ]] || fail "existing release directory has incorrect executable permissions: $installed_path"
        fi
    done < <(find "$extract/$bundle" -type f -print)
else
    mv "$extract/$bundle" "$release_directory"
fi
note "verifying the installed files"
verified_fingerprint="$(release_fingerprint "$release_directory" "$staging/verified-tree")"
integrity_staging="$install_root/.release-integrity/.$release_name.$$"
printf '%s\n' "$verified_fingerprint" > "$integrity_staging"
mv -f "$integrity_staging" "$integrity_file"
old_current="$(readlink "$install_root/current" 2>/dev/null || true)"
new_current="releases/$release_name"
if [[ -n "$old_current" && "$old_current" != "$new_current" ]]; then
    previous_link="$install_root/.previous.$$"
    ln -s "$old_current" "$previous_link"
    replace_link "$previous_link" "$install_root/previous"
fi
current_link="$install_root/.current.$$"
ln -s "$new_current" "$current_link"
replace_link "$current_link" "$install_root/current"
bin_link="$install_root/.bin.$$"
ln -s current/bin "$bin_link"
replace_link "$bin_link" "$install_root/bin"
if [[ -z "$requested_version" ]]; then
    stable_version_staging="$install_root/.stable-version.$$"
    printf '%s\n' "$version" > "$stable_version_staging"
    mv -f "$stable_version_staging" "$install_root/.stable-version"
    stable_version_staging=""
fi

echo "installed Wax $version for $target in $release_directory"
configure_recording_file_type
configure_path
echo "run 'wax --version' in a new terminal to verify the installed tools and native compile-and-link path"
echo "run 'wax vscode' to install the editor extension"
