#!/bin/bash
# Copyright (c) 2025-2026 Georgii Mishchenko. All Rights Reserved.
#
# The BloxForge helper for macOS. One thing to download, one thing to
# double-click, and buttons instead of commands.
#
# It installs the Studio plugin, then shows a small window:
#
#   [ Check for updates ]  [ Auto-update: on/off ]  [ Close ]
#
# That is the whole program. There is nothing to type, nothing to uninstall by
# hand, and no command anywhere on the website - the previous version asked a
# tester to paste shell into a terminal and then remember a flag to undo it,
# which is not something to ask of anybody.
#
# Nothing a user owns is in the plugin file: the sign-in, the chat history and
# the per-place project are kept by Studio and inside the .rbxl. Updating
# replaces one file and touches no work.
#
# --check is what the hourly timer runs. It shows no window and says nothing
# unless it actually installed something.

set -u
SERVER="${BLOXFORGE_URL:-https://bloxforge.ai}"
PLUGIN_DIR="$HOME/Documents/Roblox/Plugins"
PLUGIN="$PLUGIN_DIR/BorisAI.lua"
SUPPORT="$HOME/Library/Application Support/BloxForge"
SELF_COPY="$SUPPORT/BloxForge.command"
LOG="$SUPPORT/update.log"
AGENT_ID="ai.bloxforge.updater"
AGENT="$HOME/Library/LaunchAgents/$AGENT_ID.plist"

mkdir -p "$SUPPORT"
note() { printf '%s  %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOG"; }

installed_version() {
    [ -f "$PLUGIN" ] || { echo 0; return; }
    grep -m1 -o 'local PLUGIN_VERSION = [0-9]*' "$PLUGIN" 2>/dev/null | grep -o '[0-9]*$' || echo 0
}
field() { printf '%s' "$1" | grep -o "\"$2\"[[:space:]]*:[[:space:]]*\"\{0,1\}[^,\"}]*" | sed 's/.*[:"]//' | tail -1; }
auto_on() { [ -f "$AGENT" ]; }

# Downloads the plugin and puts it in place, or leaves the working one exactly
# as it was. Three proofs before anything moves - a half-written plugin
# installed over a good one is far worse than not updating at all.
# Echoes: "updated <version>", "current <version>", or "failed <reason>".
do_update() {
    local manifest remote sha bytes local_v temp got_bytes got_sha got_version
    manifest=$(curl -fsS --max-time 30 "$SERVER/api/plugin/manifest" 2>/dev/null) || { echo "failed could not reach the server"; return; }
    remote=$(field "$manifest" version)
    sha=$(field "$manifest" sha256)
    bytes=$(field "$manifest" bytes)
    case "$remote" in ''|*[!0-9]*) echo "failed the server did not say which version it has"; return;; esac

    local_v=$(installed_version)
    if [ "$remote" -le "$local_v" ] && [ -f "$PLUGIN" ]; then echo "current $local_v"; return; fi

    mkdir -p "$PLUGIN_DIR"
    temp="$PLUGIN.incoming"
    rm -f "$temp"
    curl -fsS --max-time 120 "$SERVER/download/plugin" -o "$temp" || { rm -f "$temp"; echo "failed the download did not finish"; return; }
    got_bytes=$(wc -c < "$temp" | tr -d ' ')
    got_sha=$(shasum -a 256 "$temp" 2>/dev/null | cut -d' ' -f1)
    got_version=$(grep -m1 -o 'local PLUGIN_VERSION = [0-9]*' "$temp" 2>/dev/null | grep -o '[0-9]*$')
    if [ -n "$bytes" ] && [ "$got_bytes" != "$bytes" ]; then rm -f "$temp"; echo "failed the download arrived incomplete"; return; fi
    if [ -n "$sha" ] && [ -n "$got_sha" ] && [ "$got_sha" != "$sha" ]; then rm -f "$temp"; echo "failed the download did not match its checksum"; return; fi
    if [ "${got_version:-0}" != "$remote" ]; then rm -f "$temp"; echo "failed the file was not the version the server promised"; return; fi
    mv -f "$temp" "$PLUGIN" || { rm -f "$temp"; echo "failed could not write to the plugins folder"; return; }
    note "updated $local_v -> $remote"
    echo "updated $remote"
}

set_auto() {
    if [ "$1" = "on" ]; then
        cp -f "$0" "$SELF_COPY" 2>/dev/null && chmod +x "$SELF_COPY"
        mkdir -p "$HOME/Library/LaunchAgents"
        cat > "$AGENT" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
    <key>Label</key><string>$AGENT_ID</string>
    <key>ProgramArguments</key><array>
        <string>/bin/bash</string><string>$SELF_COPY</string><string>--check</string>
    </array>
    <key>StartInterval</key><integer>3600</integer>
    <key>RunAtLoad</key><true/>
</dict></plist>
PLIST
        launchctl unload "$AGENT" 2>/dev/null
        launchctl load "$AGENT" 2>/dev/null
        note "auto-update on"
    else
        launchctl unload "$AGENT" 2>/dev/null
        rm -f "$AGENT"
        note "auto-update off"
    fi
}

# The hourly timer: no window, and it only speaks when it did something.
if [ "${1:-}" = "--check" ]; then
    result=$(do_update)
    case "$result" in
        updated*) osascript -e "display notification \"Updated to v${result#updated } — restart Studio to use it.\" with title \"BloxForge\"" 2>/dev/null;;
        failed*) note "$result";;
    esac
    exit 0
fi

# --- the window -------------------------------------------------------------
# osascript is on every Mac, so the program needs no runtime, no installer and
# no signing ceremony - and a tester can read the whole thing before running it.
first_run=1
while true; do
    version=$(installed_version)
    if [ "$version" = "0" ] && [ "$first_run" = "1" ]; then
        result=$(do_update)
        set_auto on
        version=$(installed_version)
        state="Plugin v$version is installed, and auto-update is on."
    elif auto_on; then
        state="Plugin v$version installed. Auto-update is ON (checks every hour)."
    else
        state="Plugin v$version installed. Auto-update is OFF."
    fi
    first_run=0

    if auto_on; then toggle="Turn auto-update off"; else toggle="Turn auto-update on"; fi
    choice=$(osascript -e "display dialog \"$state\" with title \"BloxForge\" buttons {\"Close\", \"$toggle\", \"Check for updates\"} default button \"Check for updates\"" 2>/dev/null) || exit 0
    button=${choice#*button returned:}

    case "$button" in
        "Check for updates")
            result=$(do_update)
            case "$result" in
                updated*) message="Updated to v${result#updated }. Restart Roblox Studio to use it.";;
                current*) message="You already have the newest version (v${result#current }).";;
                *)        message="Could not update: ${result#failed }. Your plugin was left exactly as it was.";;
            esac
            osascript -e "display dialog \"$message\" with title \"BloxForge\" buttons {\"OK\"} default button \"OK\"" >/dev/null 2>&1
            ;;
        "Turn auto-update on") set_auto on;;
        "Turn auto-update off") set_auto off;;
        *) exit 0;;
    esac
done
