#!/bin/sh
set -eu

umask 077
export LC_ALL=C

data_home=${XDG_DATA_HOME:-"$HOME/.local/share"}
cache_home=${XDG_CACHE_HOME:-"$HOME/.cache"}
state_home=${XDG_STATE_HOME:-"$HOME/.local/state"}
app_root=${ICQ2_APP_ROOT:-"$data_home/icq2/client"}
cache_root=${ICQ2_UPDATE_CACHE_ROOT:-"$cache_home/icq2/update"}
state_root=${ICQ2_UPDATE_STATE_ROOT:-"$state_home/icq2"}
runtime_root="$app_root/runtime"
versions_root="$app_root/versions"
public_key="$runtime_root/update-public-key.pem"
config_file="$app_root/update.conf"
log_file="$state_root/update.log"
lock_dir="$app_root/update.lock"
default_manifest_url=https://icq2.net/releases/unix/linux-x86_64/friends.manifest
max_manifest_bytes=32768
max_package_bytes=536870912
check_interval=86400
retry_interval=3600
lock_held=0
temporary_dir=

mkdir -p "$app_root" "$cache_root" "$state_root" "$runtime_root" "$versions_root"

timestamp() {
    date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || printf 'unknown-time'
}

log_message() {
    printf '%s %s\n' "$(timestamp)" "$*" >>"$log_file" 2>/dev/null || true
    size=$(wc -c <"$log_file" 2>/dev/null || printf '0')
    if [ "$size" -gt 262144 ] 2>/dev/null; then
        tail -n 200 "$log_file" >"$log_file.tmp.$$" 2>/dev/null &&
            mv -f "$log_file.tmp.$$" "$log_file"
    fi
}

fail() {
    log_message "failed: $*"
    printf '%s\n' "ICQ2 update: $*" >&2
    exit 1
}

[ "$(uname -s 2>/dev/null || true)" = Linux ] || fail "this direct updater supports Linux only"
case "$(uname -m 2>/dev/null || true)" in
    x86_64|amd64) ;;
    *) fail "this update channel supports Linux x86-64 only" ;;
esac

cleanup() {
    if [ -n "$temporary_dir" ]; then
        case "$temporary_dir" in
            "$cache_root"/check.*|"$cache_root"/stage.*)
                rm -rf -- "$temporary_dir"
                ;;
        esac
    fi
    if [ "$lock_held" -eq 1 ]; then
        rm -f -- "$lock_dir/pid" 2>/dev/null || true
        rmdir -- "$lock_dir" 2>/dev/null || true
    fi
}
trap cleanup EXIT HUP INT TERM

acquire_lock() {
    if mkdir "$lock_dir" 2>/dev/null; then
        lock_held=1
        printf '%s\n' "$$" >"$lock_dir/pid"
        return 0
    fi
    old_pid=$(sed -n '1p' "$lock_dir/pid" 2>/dev/null || true)
    case "$old_pid" in
        ''|*[!0-9]*) return 1 ;;
    esac
    if kill -0 "$old_pid" 2>/dev/null; then
        return 1
    fi
    rm -f -- "$lock_dir/pid" 2>/dev/null || return 1
    rmdir -- "$lock_dir" 2>/dev/null || return 1
    if mkdir "$lock_dir" 2>/dev/null; then
        lock_held=1
        printf '%s\n' "$$" >"$lock_dir/pid"
        return 0
    fi
    return 1
}

managed_by_package_manager() {
    [ -e "$app_root/managed-by-package-manager" ]
}

approved_https_url() {
    url=$1
    if printf '%s' "$url" | grep -Eq '[[:cntrl:][:space:]]'; then
        return 1
    fi
    case "$url" in
        *'?'*|*'#'*|*'\\'*|*'@'*) return 1 ;;
    esac
    case "$url" in
        https://icq2.net/*|https://www.icq2.net/*|https://download.icq2.net/*)
            return 0
            ;;
    esac
    return 1
}

manifest_url() {
    configured=$(sed -n 's/^manifest_url=//p' "$config_file" 2>/dev/null | sed -n '1p')
    if [ -n "$configured" ]; then
        printf '%s\n' "$configured"
    else
        printf '%s\n' "$default_manifest_url"
    fi
}

download_file() {
    url=$1
    destination=$2
    byte_limit=$3
    if [ -n "${ICQ2_UPDATE_TEST_SOURCE_DIR:-}" ]; then
        name=${url##*/}
        source_path="$ICQ2_UPDATE_TEST_SOURCE_DIR/$name"
        [ -f "$source_path" ] || return 1
        size=$(wc -c <"$source_path")
        [ "$size" -le "$byte_limit" ] || return 1
        cp -- "$source_path" "$destination"
        return 0
    fi
    command -v curl >/dev/null 2>&1 || return 1
    curl --fail --silent --show-error --proto '=https' --tlsv1.2 \
        --max-redirs 0 --connect-timeout 4 --max-time 30 \
        --max-filesize "$byte_limit" --output "$destination" "$url"
}

verify_signature() {
    content=$1
    signature=$2
    [ -f "$public_key" ] || return 1
    openssl pkeyutl -verify -pubin -inkey "$public_key" -rawin \
        -in "$content" -sigfile "$signature" >/dev/null 2>&1
}

valid_version() {
    printf '%s\n' "$1" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9][0-9]([ab])?$'
}

version_is_newer() {
    new_version=$1
    old_version=$2
    awk -v new="$new_version" -v old="$old_version" '
        function decode(v, out, suffix, n, p) {
            suffix = substr(v, length(v), 1)
            if (suffix == "a") { out[4] = 0; v = substr(v, 1, length(v)-1) }
            else if (suffix == "b") { out[4] = 1; v = substr(v, 1, length(v)-1) }
            else { out[4] = 2 }
            n = split(v, p, ".")
            if (n != 3) return 0
            out[1] = p[1] + 0; out[2] = p[2] + 0; out[3] = p[3] + 0
            return 1
        }
        BEGIN {
            if (!decode(new, a) || !decode(old, b)) exit 1
            for (i = 1; i <= 4; ++i) {
                if (a[i] > b[i]) exit 0
                if (a[i] < b[i]) exit 1
            }
            exit 1
        }
    '
}

metadata_value() {
    metadata_file=$1
    metadata_key=$2
    sed -n "s/^${metadata_key}=//p" "$metadata_file" 2>/dev/null | sed -n '1p'
}

current_version() {
    metadata_value "$app_root/current/package.env" version
}

write_atomic() {
    destination=$1
    value=$2
    printf '%s\n' "$value" >"$destination.tmp.$$"
    mv -f "$destination.tmp.$$" "$destination"
}

parse_manifest() {
    input=$1
    schema_version= product= channel= version= platform= architecture=
    published_utc= package_url= size_bytes= sha256= release_notes_url=
    seen='|'
    while IFS= read -r line || [ -n "$line" ]; do
        case "$line" in
            ''|*"$(printf '\r')"*) return 1 ;;
            *=*) ;;
            *) return 1 ;;
        esac
        key=${line%%=*}
        value=${line#*=}
        [ -n "$value" ] || return 1
        case "$seen" in *"|$key|"*) return 1 ;; esac
        seen="$seen$key|"
        case "$key" in
            schema_version) schema_version=$value ;;
            product) product=$value ;;
            channel) channel=$value ;;
            version) version=$value ;;
            platform) platform=$value ;;
            architecture) architecture=$value ;;
            published_utc) published_utc=$value ;;
            package_url) package_url=$value ;;
            size_bytes) size_bytes=$value ;;
            sha256) sha256=$value ;;
            release_notes_url) release_notes_url=$value ;;
            *) return 1 ;;
        esac
    done <"$input"
    [ "$schema_version" = 1 ] &&
        [ "$product" = icq2-unix-tui ] &&
        [ "$channel" = friends ] &&
        [ "$platform" = linux ] &&
        [ "$architecture" = x86_64 ] &&
        valid_version "$version" &&
        printf '%s\n' "$published_utc" | grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$' &&
        approved_https_url "$package_url" &&
        approved_https_url "$release_notes_url" &&
        printf '%s\n' "$size_bytes" | grep -Eq '^[0-9]+$' &&
        [ "$size_bytes" -gt 0 ] && [ "$size_bytes" -le "$max_package_bytes" ] &&
        printf '%s\n' "$sha256" | grep -Eq '^[0-9a-f]{64}$'
}

sha256_file() {
    if command -v sha256sum >/dev/null 2>&1; then
        sha256sum "$1" | awk '{print $1}'
    else
        openssl dgst -sha256 -r "$1" | awk '{print $1}'
    fi
}

safe_archive_listing() {
    archive=$1
    expected_root=$2
    tar -tzf "$archive" | awk -v root="$expected_root" '
        BEGIN { ok = 1; count = 0 }
        {
            count++
            entry = $0
            sub(/\/$/, "", entry)
            if (entry == "" || substr(entry,1,1) == "/" || index(entry,"\\") != 0) ok = 0
            n = split(entry, p, "/")
            if (p[1] != root) ok = 0
            for (i = 1; i <= n; ++i) if (p[i] == ".." || p[i] == "") ok = 0
        }
        END { exit !(ok && count > 0) }
    '
}

safe_archive_size() {
    tar -tvzf "$1" | awk '
        BEGIN { ok=1; total=0 }
        {
            if (NR > 2048 || $3 !~ /^[0-9]+$/) ok=0
            total += $3
            if (total > 1073741824) ok=0
        }
        END { exit !(ok && NR > 0) }
    '
}

stage_available_update() {
    requested_url=$(manifest_url)
    approved_https_url "$requested_url" || fail "the discovery URL is not approved"
    [ -f "$public_key" ] || fail "the trusted update public key is missing"

    temporary_dir=$(mktemp -d "$cache_root/check.XXXXXX") || fail "cannot create update workspace"
    manifest_path="$temporary_dir/manifest"
    signature_path="$temporary_dir/manifest.sig"
    download_file "$requested_url" "$manifest_path" "$max_manifest_bytes" ||
        fail "cannot download update metadata"
    download_file "$requested_url.sig" "$signature_path" 4096 ||
        fail "cannot download update metadata signature"
    [ "$(wc -c <"$manifest_path")" -le "$max_manifest_bytes" ] ||
        fail "update metadata is too large"
    verify_signature "$manifest_path" "$signature_path" ||
        fail "update metadata signature is invalid"
    parse_manifest "$manifest_path" || fail "update metadata is invalid"

    installed=$(current_version)
    [ -n "$installed" ] && valid_version "$installed" || fail "installed version is invalid"
    high_water=$(sed -n '1p' "$app_root/highest-version" 2>/dev/null || true)
    if [ -z "$high_water" ]; then high_water=$installed; fi
    valid_version "$high_water" || fail "the anti-rollback state is invalid"
    if ! version_is_newer "$version" "$high_water"; then
        log_message "checked: no newer signed version (installed=$installed highest=$high_water offered=$version)"
        return 2
    fi
    already_staged=$(sed -n '1p' "$app_root/staged-version" 2>/dev/null || true)
    if [ "$already_staged" = "$version" ] && [ -d "$versions_root/$version" ]; then
        log_message "checked: signed update $version is already staged"
        return 2
    fi

    package_path="$temporary_dir/package.tar.gz"
    download_file "$package_url" "$package_path" "$size_bytes" ||
        fail "cannot download the update package"
    actual_size=$(wc -c <"$package_path")
    [ "$actual_size" -eq "$size_bytes" ] || fail "update package size mismatch"
    actual_sha256=$(sha256_file "$package_path")
    [ "$actual_sha256" = "$sha256" ] || fail "update package hash mismatch"

    expected_root="icq2-$version-linux-$architecture"
    safe_archive_listing "$package_path" "$expected_root" ||
        fail "update package contains unsafe paths"
    safe_archive_size "$package_path" ||
        fail "update package expands beyond safety limits"
    extract_root="$temporary_dir/extracted"
    mkdir "$extract_root"
    tar --no-same-owner --no-same-permissions -xzf "$package_path" -C "$extract_root" ||
        fail "cannot extract update package"
    candidate="$extract_root/$expected_root"
    [ -d "$candidate" ] || fail "update package root is missing"
    if find "$candidate" -type l -print -quit | grep -q .; then
        fail "update package contains symbolic links"
    fi
    [ -x "$candidate/bin/icq-tui" ] || fail "update package client is missing"
    [ -x "$candidate/runtime/icq2-launcher" ] || fail "update launcher is missing"
    [ -x "$candidate/runtime/icq2-update" ] || fail "update agent is missing"
    [ -f "$candidate/runtime/update-public-key.pem" ] || fail "next trust key is missing"
    candidate_version=$(metadata_value "$candidate/package.env" version)
    candidate_platform=$(metadata_value "$candidate/package.env" platform)
    candidate_arch=$(metadata_value "$candidate/package.env" architecture)
    [ "$candidate_version" = "$version" ] && [ "$candidate_platform" = linux ] &&
        [ "$candidate_arch" = "$architecture" ] || fail "package metadata mismatch"
    reported_version=$($candidate/bin/icq-tui --version 2>/dev/null || true)
    [ "$reported_version" = "$version" ] || fail "package executable version mismatch"

    destination="$versions_root/$version"
    [ ! -e "$destination" ] || fail "offered version is already installed"
    mv "$candidate" "$destination"
    write_atomic "$app_root/staged-version" "$version"
    write_atomic "$app_root/staged-release-notes-url" "$release_notes_url"
    log_message "staged signed update $version"
    printf '%s\n' "ICQ2 update $version is ready and will activate at the next safe restart."
    return 0
}

activate_staged() {
    staged=$(sed -n '1p' "$app_root/staged-version" 2>/dev/null || true)
    [ -n "$staged" ] || return 0
    valid_version "$staged" || fail "staged version state is invalid"
    candidate="$versions_root/$staged"
    [ -x "$candidate/bin/icq-tui" ] || fail "staged client is missing"
    reported=$($candidate/bin/icq-tui --version 2>/dev/null || true)
    [ "$reported" = "$staged" ] || fail "staged client version mismatch"
    "$candidate/bin/icq-tui" --self-test >/dev/null 2>&1 ||
        fail "staged client startup check failed"

    if [ -e "$app_root/current" ] && [ ! -L "$app_root/current" ]; then
        fail "current version path is not a symbolic link"
    fi
    if [ -e "$app_root/previous" ] && [ ! -L "$app_root/previous" ]; then
        fail "previous version path is not a symbolic link"
    fi
    old_target=$(readlink "$app_root/current" 2>/dev/null || true)
    case "$old_target" in versions/*) ;; '') ;; *) fail "current version link is unsafe" ;; esac
    if [ -n "$old_target" ]; then
        ln -s "$old_target" "$app_root/previous.new.$$"
        mv -Tf "$app_root/previous.new.$$" "$app_root/previous"
    fi
    ln -s "versions/$staged" "$app_root/current.new.$$"
    mv -Tf "$app_root/current.new.$$" "$app_root/current"

    write_atomic "$app_root/highest-version" "$staged"
    write_atomic "$app_root/pending-health" "$staged"
    rm -f -- "$app_root/staged-version" "$app_root/staged-release-notes-url"
    log_message "activated update $staged"
}

promote_runtime() {
    promoted_version=$1
    promoted="$versions_root/$promoted_version"
    [ -x "$promoted/runtime/icq2-launcher" ] || fail "promoted launcher is missing"
    [ -x "$promoted/runtime/icq2-update" ] || fail "promoted update agent is missing"
    [ -f "$promoted/runtime/update-public-key.pem" ] || fail "promoted trust key is missing"
    for runtime_file in icq2-launcher icq2-update; do
        cp "$promoted/runtime/$runtime_file" "$runtime_root/$runtime_file.new.$$"
        chmod 0755 "$runtime_root/$runtime_file.new.$$"
        mv -f "$runtime_root/$runtime_file.new.$$" "$runtime_root/$runtime_file"
    done
    cp "$promoted/runtime/update-public-key.pem" "$runtime_root/update-public-key.pem.new.$$"
    chmod 0644 "$runtime_root/update-public-key.pem.new.$$"
    mv -f "$runtime_root/update-public-key.pem.new.$$" "$runtime_root/update-public-key.pem"
}

rollback_current() {
    [ -L "$app_root/current" ] && [ -L "$app_root/previous" ] ||
        fail "version links are missing or unsafe"
    previous_target=$(readlink "$app_root/previous" 2>/dev/null || true)
    current_target=$(readlink "$app_root/current" 2>/dev/null || true)
    case "$previous_target" in versions/*) ;; *) fail "no safe previous version is available" ;; esac
    case "$current_target" in versions/*) ;; *) fail "current version link is unsafe" ;; esac
    [ -x "$app_root/$previous_target/bin/icq-tui" ] || fail "previous client is missing"
    ln -s "$previous_target" "$app_root/current.new.$$"
    mv -Tf "$app_root/current.new.$$" "$app_root/current"
    ln -s "$current_target" "$app_root/previous.new.$$"
    mv -Tf "$app_root/previous.new.$$" "$app_root/previous"
    rm -f -- "$app_root/pending-health"
    log_message "rolled back from ${current_target#versions/} to ${previous_target#versions/}"
    printf '%s\n' "ICQ2 rolled back to ${previous_target#versions/}."
}

mark_healthy() {
    running_version=$1
    pending=$(sed -n '1p' "$app_root/pending-health" 2>/dev/null || true)
    if [ -n "$pending" ] && [ "$pending" = "$running_version" ]; then
        promote_runtime "$running_version"
        rm -f -- "$app_root/pending-health"
        log_message "startup health accepted for $running_version"
    fi
}

mark_start_failed() {
    running_version=$1
    pending=$(sed -n '1p' "$app_root/pending-health" 2>/dev/null || true)
    if [ -n "$pending" ] && [ "$pending" = "$running_version" ]; then
        log_message "new version $running_version exited during startup; automatic rollback"
        rollback_current >/dev/null
    fi
}

check_due() {
    now=$(date +%s 2>/dev/null || printf '0')
    last_success=$(sed -n '1p' "$app_root/last-successful-check" 2>/dev/null || printf '0')
    last_attempt=$(sed -n '1p' "$app_root/last-check-attempt" 2>/dev/null || printf '0')
    case "$now:$last_success:$last_attempt" in *[!0-9:]*) return 0 ;; esac
    [ $((now - last_success)) -ge "$check_interval" ] || return 1
    [ $((now - last_attempt)) -ge "$retry_interval" ] || return 1
    return 0
}

perform_check() {
    write_atomic "$app_root/last-check-attempt" "$(date +%s 2>/dev/null || printf '0')"
    result=0
    stage_available_update || result=$?
    if [ "$result" -eq 0 ] || [ "$result" -eq 2 ]; then
        write_atomic "$app_root/last-successful-check" "$(date +%s 2>/dev/null || printf '0')"
        return 0
    fi
    return "$result"
}

status_report() {
    installed=$(current_version)
    staged=$(sed -n '1p' "$app_root/staged-version" 2>/dev/null || true)
    previous_target=$(readlink "$app_root/previous" 2>/dev/null || true)
    previous_display=${previous_target#versions/}
    [ -n "$previous_display" ] || previous_display=none
    printf 'installed=%s\n' "${installed:-none}"
    printf 'staged=%s\n' "${staged:-none}"
    printf 'previous=%s\n' "$previous_display"
    if managed_by_package_manager; then
        printf 'update_mode=package-manager\n'
    else
        printf 'update_mode=direct\n'
    fi
}

command_name=${1:---background}
case "$command_name" in
    --background)
        managed_by_package_manager && exit 0
        check_due || exit 0
        acquire_lock || exit 0
        perform_check
        ;;
    --check)
        managed_by_package_manager && fail "updates are managed by the system package manager"
        acquire_lock || fail "another update operation is already running"
        perform_check
        ;;
    --activate-staged)
        acquire_lock || exit 0
        activate_staged
        ;;
    --rollback)
        acquire_lock || fail "another update operation is already running"
        rollback_current
        ;;
    --mark-healthy)
        [ "$#" -eq 2 ] || fail "--mark-healthy requires a version"
        acquire_lock || exit 0
        mark_healthy "$2"
        ;;
    --mark-start-failed)
        [ "$#" -eq 2 ] || fail "--mark-start-failed requires a version"
        acquire_lock || exit 0
        mark_start_failed "$2"
        ;;
    --status)
        status_report
        ;;
    *)
        printf '%s\n' "Usage: icq2-update --check|--status|--rollback" >&2
        exit 2
        ;;
esac
