#!/bin/sh
# Capsem installer -- downloads the stable binary package and installs it.
#   macOS: downloads .pkg, installs via the native installer command
#   Linux: downloads .deb, installs via apt
# Usage: curl -fsSL https://capsem.org/install.sh | sh
set -eu

CAPSEM_CHANNEL="${CAPSEM_CHANNEL:-stable}"
CAPSEM_RELEASE_BASE_URL="${CAPSEM_RELEASE_BASE_URL:-https://release.capsem.org}"
CAPSEM_MANIFEST_URL="${CAPSEM_MANIFEST_URL:-${CAPSEM_RELEASE_BASE_URL}/assets/${CAPSEM_CHANNEL}/manifest.json}"
CAPSEM_INSTALL_USER_REQUEST_DIR="/var/run/capsem"
CAPSEM_INSTALL_USER_REQUEST="$CAPSEM_INSTALL_USER_REQUEST_DIR/install-user"
CAPSEM_INSTALL_MANIFEST_PAYLOAD="$CAPSEM_INSTALL_USER_REQUEST_DIR/install-manifest.json"
CAPSEM_INSTALL_MANIFEST_REQUEST="$CAPSEM_INSTALL_USER_REQUEST_DIR/install-manifest"

# -- Testable functions ------------------------------------------------------
# These functions can be unit-tested by sourcing this script with
# __INSTALL_SH_SOURCED=1, which skips the main entry point.

detect_os() {
    _KERNEL="$(uname -s)"
    case "$_KERNEL" in
        Darwin) OS="darwin" ;;
        Linux)  OS="linux" ;;
        *)
            echo "Error: unsupported operating system: $_KERNEL. Capsem supports macOS and Linux." >&2
            return 1
            ;;
    esac
}

detect_arch() {
    _MACHINE="$(uname -m)"
    case "$OS" in
        darwin)
            case "$_MACHINE" in
                arm64) ARCH="arm64" ;;
                *)
                    echo "Error: Capsem on macOS requires Apple Silicon (arm64). Detected: $_MACHINE" >&2
                    return 1
                    ;;
            esac
            ;;
        linux)
            case "$_MACHINE" in
                x86_64|amd64)    ARCH="amd64" ;;
                aarch64|arm64)   ARCH="arm64" ;;
                *)
                    echo "Error: unsupported architecture: $_MACHINE. Capsem supports x86_64 and arm64." >&2
                    return 1
                    ;;
            esac
            ;;
    esac
}

find_asset_url() {
    _manifest_json="$1"
    _os="$2"
    _arch="$3"
    _manifest_arch_alias=""
    case "$_os" in
        darwin)
            _platform="macos"
            _kind="macos_pkg"
            _manifest_arch="arm64"
            _name_suffix=".pkg"
            ;;
        linux)
            _platform="linux"
            _kind="debian_package"
            case "$_arch" in
                amd64)
                    _manifest_arch="amd64"
                    _manifest_arch_alias="x86_64"
                    _name_suffix="_amd64.deb"
                    ;;
                arm64)
                    _manifest_arch="arm64"
                    _name_suffix="_arm64.deb"
                    ;;
                *)
                    echo "Error: no matching asset found for $_os/$_arch in the ${CAPSEM_CHANNEL} release channel." >&2
                    return 1
                    ;;
            esac
            ;;
    esac

    _asset_record="$(printf '%s\n' "$_manifest_json" | awk \
        -v platform="$_platform" \
        -v kind="$_kind" \
        -v arch="$_manifest_arch" \
        -v arch_alias="$_manifest_arch_alias" '
        function count_char(text, char, i, n) {
            n = 0
            for (i = 1; i <= length(text); i++) {
                if (substr(text, i, 1) == char) {
                    n++
                }
            }
            return n
        }
        BEGIN {
            platform_re = "\"" "platform" "\"" "[[:space:]]*:[[:space:]]*\"" platform "\""
            kind_re = "\"" "kind" "\"" "[[:space:]]*:[[:space:]]*\"" kind "\""
            arch_re = "\"" "architecture" "\"" "[[:space:]]*:[[:space:]]*\"" arch "\""
            arch_alias_re = "\"" "architecture" "\"" "[[:space:]]*:[[:space:]]*\"" arch_alias "\""
            status_re = "\"" "status" "\"" "[[:space:]]*:[[:space:]]*\"current\""
        }
        /"packages"[[:space:]]*:/ {
            in_packages = 1
            next
        }
        in_packages {
            if (!in_package && $0 ~ /^[[:space:]]*\{/) {
                in_package = 1
                depth = 0
                block = ""
            }
            if (in_package) {
                block = block $0 "\n"
                depth += count_char($0, "{") - count_char($0, "}")
                if (depth == 0) {
                    if (block ~ platform_re && block ~ kind_re \
                        && (block ~ arch_re || (arch_alias != "" && block ~ arch_alias_re)) \
                        && block ~ status_re) {
                        printf "%s", block
                        exit
                    }
                    in_package = 0
                    block = ""
                }
            }
        }
    ')"

    _asset_fields="$(printf '%s\n' "$_asset_record" | awk '
        function count_char(text, char, i, n) {
            n = 0
            for (i = 1; i <= length(text); i++) {
                if (substr(text, i, 1) == char) n++
            }
            return n
        }
        function string_value(text, parts) {
            split(text, parts, "\"")
            return parts[4]
        }
        function sha256_value(text, value) {
            value = text
            sub(/^.*"sha256"[[:space:]]*:[[:space:]]*"/, "", value)
            sub(/".*$/, "", value)
            return value
        }
        {
            if (depth == 1 && $0 ~ /"url"[[:space:]]*:/) {
                url = string_value($0)
            } else if (depth == 1 && $0 ~ /"version"[[:space:]]*:/) {
                version = string_value($0)
            } else if (depth == 1 && $0 ~ /"name"[[:space:]]*:/) {
                name = string_value($0)
            } else if (depth == 1 && $0 ~ /"bytes"[[:space:]]*:/) {
                bytes = $0
                sub(/^.*"bytes"[[:space:]]*:[[:space:]]*/, "", bytes)
                sub(/[[:space:],].*$/, "", bytes)
            } else if (depth == 1 && $0 ~ /"digest"[[:space:]]*:[[:space:]]*\{/) {
                in_package_digest = 1
                if ($0 ~ /"sha256"[[:space:]]*:/) sha256 = sha256_value($0)
            } else if (in_package_digest && depth == 2 && $0 ~ /"sha256"[[:space:]]*:/) {
                sha256 = sha256_value($0)
            }

            depth += count_char($0, "{") - count_char($0, "}")
            if (in_package_digest && depth <= 1) in_package_digest = 0
        }
        END { printf "%s|%s|%s|%s|%s", url, version, name, bytes, sha256 }
    ')"
    ASSET_URL="${_asset_fields%%|*}"
    _asset_fields="${_asset_fields#*|}"
    ASSET_VERSION="${_asset_fields%%|*}"
    _asset_fields="${_asset_fields#*|}"
    ASSET_NAME="${_asset_fields%%|*}"
    _asset_fields="${_asset_fields#*|}"
    ASSET_BYTES="${_asset_fields%%|*}"
    ASSET_SHA256="${_asset_fields#*|}"

    if [ -z "$ASSET_URL" ] || [ -z "$ASSET_VERSION" ] || [ -z "$ASSET_NAME" ] \
        || [ -z "$ASSET_BYTES" ] || [ -z "$ASSET_SHA256" ]; then
        echo "Error: no matching asset found for $_os/$_arch in the ${CAPSEM_CHANNEL} release channel." >&2
        return 1
    fi
    case "$ASSET_BYTES" in
        *[!0-9]*)
            echo "Error: release channel package $ASSET_NAME has an invalid byte size: $ASSET_BYTES" >&2
            return 1
            ;;
    esac
    case "$ASSET_SHA256" in
        *[!0-9a-fA-F]*|"")
            echo "Error: release channel package $ASSET_NAME has an invalid SHA-256 digest." >&2
            return 1
            ;;
    esac
    if [ "${#ASSET_SHA256}" -ne 64 ]; then
        echo "Error: release channel package $ASSET_NAME has an invalid SHA-256 digest." >&2
        return 1
    fi
    case "$ASSET_NAME" in
        *"$_name_suffix") ;;
        *)
            echo "Error: release channel selected unexpected package $ASSET_NAME for $_os/$_arch." >&2
            return 1
            ;;
    esac
    case "$ASSET_URL" in
        http://*|https://*) ;;
        *)
            echo "Error: release channel package $ASSET_NAME has an invalid URL: $ASSET_URL" >&2
            return 1
            ;;
    esac
}

verify_package() {
    _package_path="$1"
    _package_name="$2"
    _expected_bytes="$3"
    _expected_sha256="$4"

    _actual_bytes="$(wc -c < "$_package_path" | tr -d '[:space:]')"
    if [ "$_actual_bytes" != "$_expected_bytes" ]; then
        echo "Error: package integrity check failed for $_package_name: expected $_expected_bytes bytes, got $_actual_bytes." >&2
        return 1
    fi

    if command -v sha256sum >/dev/null 2>&1; then
        _actual_sha256="$(sha256sum "$_package_path" | awk '{ print $1 }')"
    elif command -v shasum >/dev/null 2>&1; then
        _actual_sha256="$(shasum -a 256 "$_package_path" | awk '{ print $1 }')"
    else
        echo "Error: package integrity check failed: sha256sum or shasum is required." >&2
        return 1
    fi
    _actual_sha256="$(printf '%s' "$_actual_sha256" | tr '[:upper:]' '[:lower:]')"
    _expected_sha256="$(printf '%s' "$_expected_sha256" | tr '[:upper:]' '[:lower:]')"
    if [ "$_actual_sha256" != "$_expected_sha256" ]; then
        echo "Error: package integrity check failed for $_package_name: SHA-256 mismatch." >&2
        return 1
    fi
    echo "Verified $_package_name ($_actual_bytes bytes, SHA-256 $_actual_sha256)."
}

fetch_release_manifest() {
    _manifest_path="$1"
    if [ -z "$CAPSEM_MANIFEST_URL" ]; then
        echo "Error: CAPSEM_MANIFEST_URL is empty." >&2
        return 1
    fi
    curl -fsSL -o "$_manifest_path" "$CAPSEM_MANIFEST_URL"
}

ensure_install_workspace() {
    if [ -z "${TMPDIR_INSTALL:-}" ]; then
        TMPDIR_INSTALL="$(mktemp -d)"
    fi
    MANIFEST_PATH="${MANIFEST_PATH:-${TMPDIR_INSTALL}/manifest.json}"
}

prepare_install_manifest() {
    if [ ! -f "$MANIFEST_PATH" ] || [ -L "$MANIFEST_PATH" ]; then
        echo "Error: verified release manifest is not a regular local file." >&2
        return 1
    fi
    _request_source="${TMPDIR_INSTALL}/install-manifest"
    printf '%s\nfile://%s\n' \
        "$CAPSEM_MANIFEST_URL" "$CAPSEM_INSTALL_MANIFEST_PAYLOAD" > "$_request_source"
    chmod 0600 "$_request_source"
    sudo /usr/bin/install -d -o root -m 0700 "$CAPSEM_INSTALL_USER_REQUEST_DIR"
    sudo /usr/bin/install -o root -m 0600 \
        "$MANIFEST_PATH" "$CAPSEM_INSTALL_MANIFEST_PAYLOAD"
    INSTALL_MANIFEST_REQUEST_WRITTEN=1
    sudo /usr/bin/install -o root -m 0600 \
        "$_request_source" "$CAPSEM_INSTALL_MANIFEST_REQUEST"
}

clear_install_manifest_request() {
    if [ "${INSTALL_MANIFEST_REQUEST_WRITTEN:-0}" = "1" ]; then
        sudo rm -f "$CAPSEM_INSTALL_MANIFEST_REQUEST" "$CAPSEM_INSTALL_MANIFEST_PAYLOAD"
        INSTALL_MANIFEST_REQUEST_WRITTEN=0
    fi
}

cleanup_install() {
    clear_macos_install_user_request || true
    clear_install_manifest_request || true
    if [ -n "${TMPDIR_INSTALL:-}" ]; then
        rm -rf "$TMPDIR_INSTALL"
    fi
}

prepare_macos_install_user() {
    _install_user="${SUDO_USER:-$(id -un)}"
    case "$_install_user" in
        ""|root|*[!A-Za-z0-9._-]*)
            echo "Error: macOS package installation requires a non-root target user." >&2
            return 1
            ;;
    esac
    id -u "$_install_user" >/dev/null 2>&1 || {
        echo "Error: macOS target user does not exist: $_install_user" >&2
        return 1
    }
    _request_source="${TMPDIR_INSTALL}/install-user"
    printf '%s\n' "$_install_user" > "$_request_source"
    chmod 0600 "$_request_source"
    sudo /usr/bin/install -d -o root -g wheel -m 0700 "$CAPSEM_INSTALL_USER_REQUEST_DIR"
    sudo /usr/bin/install -o root -g wheel -m 0600 "$_request_source" "$CAPSEM_INSTALL_USER_REQUEST"
}

clear_macos_install_user_request() {
    if [ "${MACOS_INSTALL_USER_REQUEST_WRITTEN:-0}" = "1" ]; then
        sudo rm -f "$CAPSEM_INSTALL_USER_REQUEST"
        MACOS_INSTALL_USER_REQUEST_WRITTEN=0
    fi
}

stop_existing_capsem() {
    if [ -x "$HOME/.capsem/bin/capsem" ]; then
        "$HOME/.capsem/bin/capsem" stop >/dev/null 2>&1 || true
    fi
}

install_macos() {
    _pkg_url="$1"
    _version="$2"
    _expected_bytes="$3"
    _expected_sha256="$4"
    _package_name="$5"

    ensure_install_workspace
    PKG_PATH="${TMPDIR_INSTALL}/Capsem.pkg"
    trap cleanup_install EXIT

    echo "Downloading $_pkg_url..."
    curl -fSL --progress-bar -o "$PKG_PATH" "$_pkg_url"
    verify_package "$PKG_PATH" "$_package_name" "$_expected_bytes" "$_expected_sha256"

    echo "Installing .pkg package (may prompt for sudo password)..."
    stop_existing_capsem
    prepare_install_manifest
    prepare_macos_install_user
    MACOS_INSTALL_USER_REQUEST_WRITTEN=1
    sudo /usr/sbin/installer -pkg "$PKG_PATH" -target /
    clear_macos_install_user_request

    echo ""
    echo "Capsem $_version installed."
    echo "Open a new terminal and run: capsem shell"
}

install_linux() {
    _deb_url="$1"
    _version="$2"
    _expected_bytes="$3"
    _expected_sha256="$4"
    _package_name="$5"

    ensure_install_workspace
    DEB_PATH="${TMPDIR_INSTALL}/capsem.deb"
    trap cleanup_install EXIT

    echo "Downloading $_deb_url..."
    curl -fSL --progress-bar -o "$DEB_PATH" "$_deb_url"
    verify_package "$DEB_PATH" "$_package_name" "$_expected_bytes" "$_expected_sha256"

    echo "Installing .deb package (may prompt for sudo password)..."
    stop_existing_capsem
    prepare_install_manifest
    sudo apt install -y "$DEB_PATH"

    echo ""
    echo "Capsem $_version installed."
    echo "Run it with: capsem"
}

# -- Main entry point --------------------------------------------------------

if [ "${__INSTALL_SH_SOURCED:-}" = "1" ]; then
    return 0 2>/dev/null || true
fi

# Preflight: curl required
if ! command -v curl >/dev/null 2>&1; then
    echo "Error: curl is required but not found." >&2
    exit 1
fi

# Detect platform
detect_os
detect_arch

# OS-specific preflight
case "$OS" in
    darwin)
        MACOS_VERSION="$(sw_vers -productVersion)"
        MACOS_MAJOR="$(echo "$MACOS_VERSION" | cut -d. -f1)"
        if [ "$MACOS_MAJOR" -lt 14 ]; then
            echo "Error: Capsem requires macOS 14 (Sonoma) or later. Detected: $MACOS_VERSION" >&2
            exit 1
        fi
        ;;
    linux)
        if ! command -v apt >/dev/null 2>&1; then
            echo "Error: apt is required for installation. Capsem provides .deb packages for Debian/Ubuntu." >&2
            exit 1
        fi
        ;;
esac

# Fetch stable release-channel manifest. The asset lane may be newer than the
# binary lane, so do not use GitHub's "latest release" endpoint here.
echo "Fetching Capsem ${CAPSEM_CHANNEL} release channel..."
ensure_install_workspace
trap cleanup_install EXIT
fetch_release_manifest "$MANIFEST_PATH"
RELEASE_MANIFEST="$(cat "$MANIFEST_PATH")"

# Find the right asset
find_asset_url "$RELEASE_MANIFEST" "$OS" "$ARCH"
VERSION="$ASSET_VERSION"
echo "Installing Capsem $VERSION from ${CAPSEM_CHANNEL}..."

# Install
case "$OS" in
    darwin) install_macos "$ASSET_URL" "$VERSION" "$ASSET_BYTES" "$ASSET_SHA256" "$ASSET_NAME" ;;
    linux)  install_linux "$ASSET_URL" "$VERSION" "$ASSET_BYTES" "$ASSET_SHA256" "$ASSET_NAME" ;;
esac
