#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# LuCI RPC backend for Streamlit Platform
# Copyright (C) 2025 CyberMind.fr

. /lib/functions.sh
. /usr/share/libubox/jshn.sh

CONFIG="streamlit"
LXC_NAME="streamlit"
LXC_PATH="/srv/lxc"
APPS_PATH="/srv/streamlit/apps"

# JSON helpers
json_init_obj() { json_init; json_add_object "result"; }
json_close_obj() { json_close_object; json_dump; }

json_error() {
	json_init_obj
	json_add_boolean "success" 0
	json_add_string "message" "$1"
	json_close_obj
}

json_success() {
	json_init_obj
	json_add_boolean "success" 1
	[ -n "$1" ] && json_add_string "message" "$1"
	json_close_obj
}

# Check if container is running
lxc_running() {
	lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
}

# Check if container exists
lxc_exists() {
	[ -f "$LXC_PATH/$LXC_NAME/config" ] && [ -d "$LXC_PATH/$LXC_NAME/rootfs" ]
}

# Get service status
get_status() {
	local enabled running installed uptime
	local http_port data_path memory_limit active_app

	config_load "$CONFIG"
	config_get enabled main enabled "0"
	config_get http_port main http_port "8501"
	config_get data_path main data_path "/srv/streamlit"
	config_get memory_limit main memory_limit "512M"
	config_get active_app main active_app "hello"

	running="false"
	installed="false"
	uptime=""

	if lxc_exists; then
		installed="true"
	fi

	if lxc_running; then
		running="true"
		uptime=$(lxc-info -n "$LXC_NAME" 2>/dev/null | grep -i "cpu use" | head -1 | awk '{print $3}')
	fi

	# Count apps
	local app_count=0
	APPS_PATH="$data_path/apps"
	if [ -d "$APPS_PATH" ]; then
		app_count=$(ls -1 "$APPS_PATH"/*.py 2>/dev/null | wc -l)
	fi

	# Get LAN IP for URL
	local lan_ip
	lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")

	json_init_obj
	json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )"
	json_add_boolean "running" "$( [ "$running" = "true" ] && echo 1 || echo 0 )"
	json_add_boolean "installed" "$( [ "$installed" = "true" ] && echo 1 || echo 0 )"
	json_add_string "uptime" "$uptime"
	json_add_int "http_port" "$http_port"
	json_add_string "data_path" "$data_path"
	json_add_string "memory_limit" "$memory_limit"
	json_add_string "active_app" "$active_app"
	json_add_int "app_count" "$app_count"
	json_add_string "web_url" "http://${lan_ip}:${http_port}"
	json_add_string "container_name" "$LXC_NAME"
	json_close_obj
}

# Get configuration
get_config() {
	local http_port http_host data_path memory_limit enabled active_app
	local headless gather_stats theme_base theme_primary

	config_load "$CONFIG"

	# Main settings
	config_get http_port main http_port "8501"
	config_get http_host main http_host "0.0.0.0"
	config_get data_path main data_path "/srv/streamlit"
	config_get memory_limit main memory_limit "512M"
	config_get enabled main enabled "0"
	config_get active_app main active_app "hello"

	# Server settings
	config_get headless server headless "true"
	config_get gather_stats server browser_gather_usage_stats "false"
	config_get theme_base server theme_base "dark"
	config_get theme_primary server theme_primary_color "#0ff"

	json_init_obj
	json_add_object "main"
	json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )"
	json_add_int "http_port" "$http_port"
	json_add_string "http_host" "$http_host"
	json_add_string "data_path" "$data_path"
	json_add_string "memory_limit" "$memory_limit"
	json_add_string "active_app" "$active_app"
	json_close_object

	json_add_object "server"
	json_add_boolean "headless" "$( [ "$headless" = "true" ] && echo 1 || echo 0 )"
	json_add_boolean "browser_gather_usage_stats" "$( [ "$gather_stats" = "true" ] && echo 1 || echo 0 )"
	json_add_string "theme_base" "$theme_base"
	json_add_string "theme_primary_color" "$theme_primary"
	json_close_object

	json_close_obj
}

# Save configuration
save_config() {
	read -r input

	local http_port http_host data_path memory_limit enabled active_app
	local headless gather_stats theme_base theme_primary

	http_port=$(echo "$input" | jsonfilter -e '@.http_port' 2>/dev/null)
	http_host=$(echo "$input" | jsonfilter -e '@.http_host' 2>/dev/null)
	data_path=$(echo "$input" | jsonfilter -e '@.data_path' 2>/dev/null)
	memory_limit=$(echo "$input" | jsonfilter -e '@.memory_limit' 2>/dev/null)
	enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null)
	active_app=$(echo "$input" | jsonfilter -e '@.active_app' 2>/dev/null)
	headless=$(echo "$input" | jsonfilter -e '@.headless' 2>/dev/null)
	gather_stats=$(echo "$input" | jsonfilter -e '@.browser_gather_usage_stats' 2>/dev/null)
	theme_base=$(echo "$input" | jsonfilter -e '@.theme_base' 2>/dev/null)
	theme_primary=$(echo "$input" | jsonfilter -e '@.theme_primary_color' 2>/dev/null)

	[ -n "$http_port" ] && uci set "${CONFIG}.main.http_port=$http_port"
	[ -n "$http_host" ] && uci set "${CONFIG}.main.http_host=$http_host"
	[ -n "$data_path" ] && uci set "${CONFIG}.main.data_path=$data_path"
	[ -n "$memory_limit" ] && uci set "${CONFIG}.main.memory_limit=$memory_limit"
	[ -n "$enabled" ] && uci set "${CONFIG}.main.enabled=$enabled"
	[ -n "$active_app" ] && uci set "${CONFIG}.main.active_app=$active_app"
	[ -n "$headless" ] && uci set "${CONFIG}.server.headless=$headless"
	[ -n "$gather_stats" ] && uci set "${CONFIG}.server.browser_gather_usage_stats=$gather_stats"
	[ -n "$theme_base" ] && uci set "${CONFIG}.server.theme_base=$theme_base"
	[ -n "$theme_primary" ] && uci set "${CONFIG}.server.theme_primary_color=$theme_primary"

	uci commit "$CONFIG"

	json_success "Configuration saved"
}

# Start service
start_service() {
	if lxc_running; then
		json_error "Service is already running"
		return
	fi

	if ! lxc_exists; then
		json_error "Container not installed. Run install first."
		return
	fi

	/etc/init.d/streamlit start >/dev/null 2>&1 &

	sleep 2
	if lxc_running; then
		json_success "Service started"
	else
		json_error "Failed to start service"
	fi
}

# Stop service
stop_service() {
	if ! lxc_running; then
		json_error "Service is not running"
		return
	fi

	/etc/init.d/streamlit stop >/dev/null 2>&1

	sleep 2
	if ! lxc_running; then
		json_success "Service stopped"
	else
		json_error "Failed to stop service"
	fi
}

# Restart service
restart_service() {
	/etc/init.d/streamlit restart >/dev/null 2>&1 &

	sleep 3
	if lxc_running; then
		json_success "Service restarted"
	else
		json_error "Service restart failed"
	fi
}

# Install Streamlit
install() {
	if lxc_exists; then
		json_error "Already installed. Use update to refresh."
		return
	fi

	# Run install in background
	/usr/sbin/streamlitctl install >/var/log/streamlit-install.log 2>&1 &

	json_init_obj
	json_add_boolean "started" 1
	json_add_string "message" "Installation started in background"
	json_add_string "log_file" "/var/log/streamlit-install.log"
	json_close_obj
}

# Uninstall Streamlit
uninstall() {
	/usr/sbin/streamlitctl uninstall >/dev/null 2>&1

	if ! lxc_exists; then
		json_success "Uninstalled successfully"
	else
		json_error "Uninstall failed"
	fi
}

# Update Streamlit
update() {
	if ! lxc_exists; then
		json_error "Not installed. Run install first."
		return
	fi

	# Run update in background
	/usr/sbin/streamlitctl update >/var/log/streamlit-update.log 2>&1 &

	json_init_obj
	json_add_boolean "started" 1
	json_add_string "message" "Update started in background"
	json_add_string "log_file" "/var/log/streamlit-update.log"
	json_close_obj
}

# Get logs
get_logs() {
	read -r input
	local lines
	lines=$(echo "$input" | jsonfilter -e '@.lines' 2>/dev/null)
	[ -z "$lines" ] && lines=100

	local data_path
	config_load "$CONFIG"
	config_get data_path main data_path "/srv/streamlit"

	json_init_obj
	json_add_array "logs"

	# Get container logs from data path
	if [ -d "$data_path/logs" ]; then
		local logfile
		for logfile in "$data_path/logs"/*.log; do
			[ -f "$logfile" ] || continue
			tail -n "$lines" "$logfile" 2>/dev/null | while IFS= read -r line; do
				json_add_string "" "$line"
			done
		done
	fi

	# Also check install/update logs
	for logfile in /var/log/streamlit-install.log /var/log/streamlit-update.log; do
		[ -f "$logfile" ] || continue
		tail -n 50 "$logfile" 2>/dev/null | while IFS= read -r line; do
			json_add_string "" "$line"
		done
	done

	json_close_array
	json_close_obj
}

# List apps
list_apps() {
	local data_path active_app
	config_load "$CONFIG"
	config_get data_path main data_path "/srv/streamlit"
	config_get active_app main active_app "hello"

	APPS_PATH="$data_path/apps"
	local seen=""

	json_init_obj
	json_add_array "apps"

	if [ -d "$APPS_PATH" ]; then
		# Scan top-level .py files
		for app in "$APPS_PATH"/*.py; do
			[ -f "$app" ] || continue
			local name=$(basename "$app" .py)
			local display_name=$(uci -q get "${CONFIG}.${name}.name")
			[ -z "$display_name" ] && display_name="$name"
			local size=$(ls -la "$app" 2>/dev/null | awk '{print $5}')
			local mtime=$(stat -c %Y "$app" 2>/dev/null || echo "0")

			local is_active=0
			[ "$name" = "$active_app" ] && is_active=1
			seen="$seen $name "

			json_add_object ""
			json_add_string "id" "$name"
			json_add_string "name" "$display_name"
			json_add_string "path" "$app"
			json_add_string "size" "$size"
			json_add_int "mtime" "$mtime"
			json_add_boolean "active" "$is_active"
			json_close_object
		done

		# Scan subdirectories (ZIP-uploaded apps)
		for dir in "$APPS_PATH"/*/; do
			[ -d "$dir" ] || continue
			local dirname=$(basename "$dir")
			# Skip Streamlit multi-page convention dir and hidden dirs
			case "$dirname" in pages|.*) continue ;; esac
			# Skip if already seen as a top-level .py
			case "$seen" in *" $dirname "*) continue ;; esac

			# Prefer app.py as main entry point, fall back to first .py
			local main_py=""
			[ -f "$dir/app.py" ] && main_py="$dir/app.py"
			[ -z "$main_py" ] && main_py=$(find "$dir" -maxdepth 1 -name "*.py" -type f | head -1)
			[ -z "$main_py" ] && main_py=$(find "$dir" -maxdepth 2 -name "*.py" -type f | head -1)
			[ -z "$main_py" ] && continue

			local display_name=$(uci -q get "${CONFIG}.${dirname}.name")
			[ -z "$display_name" ] && display_name="$dirname"
			local size=$(stat -c %s "$main_py" 2>/dev/null || echo "0")
			local mtime=$(stat -c %Y "$main_py" 2>/dev/null || echo "0")

			local is_active=0
			[ "$dirname" = "$active_app" ] && is_active=1

			json_add_object ""
			json_add_string "id" "$dirname"
			json_add_string "name" "$display_name"
			json_add_string "path" "$main_py"
			json_add_string "size" "$size"
			json_add_int "mtime" "$mtime"
			json_add_boolean "active" "$is_active"
			json_close_object
		done
	fi

	json_close_array
	json_add_string "active_app" "$active_app"
	json_add_string "apps_path" "$APPS_PATH"
	json_close_obj
}

# Add app
add_app() {
	read -r input
	local name path
	name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
	path=$(echo "$input" | jsonfilter -e '@.path' 2>/dev/null)

	if [ -z "$name" ] || [ -z "$path" ]; then
		json_error "Missing name or path"
		return
	fi

	# Sanitize name for UCI
	name=$(echo "$name" | sed 's/[^a-zA-Z0-9_]/_/g; s/^_*//; s/_*$//')

	uci set "${CONFIG}.${name}=app"
	uci set "${CONFIG}.${name}.name=$name"
	uci set "${CONFIG}.${name}.path=$path"
	uci set "${CONFIG}.${name}.enabled=1"
	uci commit "$CONFIG"

	json_success "App added: $name"
}

# Remove app
remove_app() {
	read -r input
	local name
	name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)

	if [ -z "$name" ]; then
		json_error "Missing app name"
		return
	fi

	local data_path
	config_load "$CONFIG"
	config_get data_path main data_path "/srv/streamlit"

	# Remove app files (top-level .py or subdirectory)
	if [ -f "$data_path/apps/${name}.py" ]; then
		rm -f "$data_path/apps/${name}.py"
		rm -f "$data_path/apps/${name}.requirements.txt"
	fi
	if [ -d "$data_path/apps/${name}" ]; then
		rm -rf "$data_path/apps/${name}"
	fi

	# Remove UCI config
	uci -q delete "${CONFIG}.${name}"
	uci commit "$CONFIG"

	json_success "App removed: $name"
}

# Set active app
set_active_app() {
	read -r input
	local name
	name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)

	if [ -z "$name" ]; then
		json_error "Missing app name"
		return
	fi

	uci set "${CONFIG}.main.active_app=$name"
	uci commit "$CONFIG"

	if [ $? -eq 0 ]; then
		json_success "Active app set to: $name"
	else
		json_error "Failed to set active app"
	fi
}

# Get app details
get_app() {
	read -r input
	local name
	name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)

	if [ -z "$name" ]; then
		json_error "Missing app name"
		return
	fi

	local data_path active_app
	config_load "$CONFIG"
	config_get data_path main data_path "/srv/streamlit"
	config_get active_app main active_app "hello"

	local app_file="$data_path/apps/${name}.py"

	if [ ! -f "$app_file" ]; then
		json_error "App not found"
		return
	fi

	local size=$(ls -la "$app_file" 2>/dev/null | awk '{print $5}')
	local mtime=$(stat -c %Y "$app_file" 2>/dev/null || echo "0")
	local lines=$(wc -l < "$app_file" 2>/dev/null || echo "0")

	local is_active=0
	[ "$name" = "$active_app" ] && is_active=1

	json_init_obj
	json_add_string "name" "$name"
	json_add_string "path" "$app_file"
	json_add_string "size" "$size"
	json_add_int "mtime" "$mtime"
	json_add_int "lines" "$lines"
	json_add_boolean "active" "$is_active"
	json_close_obj
}

# Upload app (receive base64 content)
# NOTE: uhttpd-mod-ubus has a 64KB JSON body limit.
# Small files (<40KB) go through RPC directly.
# Larger files use chunked upload: upload_chunk + upload_finalize.
upload_app() {
	local tmpinput="/tmp/rpcd_upload_$$.json"
	cat > "$tmpinput"

	local name
	name=$(jsonfilter -i "$tmpinput" -e '@.name' 2>/dev/null)
	name=$(echo "$name" | sed 's/[^a-zA-Z0-9_]/_/g; s/^_*//; s/_*$//')

	if [ -z "$name" ]; then
		rm -f "$tmpinput"
		json_error "Missing name"
		return
	fi

	local b64file="/tmp/rpcd_b64_$$.txt"
	jsonfilter -i "$tmpinput" -e '@.content' > "$b64file" 2>/dev/null
	rm -f "$tmpinput"

	if [ ! -s "$b64file" ]; then
		rm -f "$b64file"
		json_error "Missing content"
		return
	fi

	local data_path
	config_load "$CONFIG"
	config_get data_path main data_path "/srv/streamlit"

	local app_file="$data_path/apps/${name}.py"
	mkdir -p "$data_path/apps"

	base64 -d < "$b64file" > "$app_file" 2>/dev/null
	local rc=$?
	rm -f "$b64file"

	if [ $rc -eq 0 ] && [ -s "$app_file" ]; then
		uci set "${CONFIG}.${name}=app"
		uci set "${CONFIG}.${name}.name=$name"
		uci set "${CONFIG}.${name}.path=${name}.py"
		uci set "${CONFIG}.${name}.enabled=1"
		uci commit "$CONFIG"
		json_success "App uploaded: $name"
	else
		rm -f "$app_file"
		json_error "Failed to decode app content"
	fi
}

# Chunked upload: receive a base64 chunk and append to temp file
upload_chunk() {
	local tmpinput="/tmp/rpcd_chunk_$$.json"
	cat > "$tmpinput"

	local name chunk_data chunk_index
	name=$(jsonfilter -i "$tmpinput" -e '@.name' 2>/dev/null)
	chunk_data=$(jsonfilter -i "$tmpinput" -e '@.data' 2>/dev/null)
	chunk_index=$(jsonfilter -i "$tmpinput" -e '@.index' 2>/dev/null)
	rm -f "$tmpinput"

	name=$(echo "$name" | sed 's/[^a-zA-Z0-9_]/_/g; s/^_*//; s/_*$//')

	if [ -z "$name" ] || [ -z "$chunk_data" ]; then
		json_error "Missing name or data"
		return
	fi

	local staging="/tmp/streamlit_upload_${name}.b64"

	# First chunk: create new file; subsequent: append
	if [ "$chunk_index" = "0" ]; then
		printf '%s' "$chunk_data" > "$staging"
	else
		printf '%s' "$chunk_data" >> "$staging"
	fi

	json_success "Chunk $chunk_index received"
}

# Finalize chunked upload: decode accumulated base64 and save
upload_finalize() {
	local tmpinput="/tmp/rpcd_finalize_$$.json"
	cat > "$tmpinput"

	local name is_zip
	name=$(jsonfilter -i "$tmpinput" -e '@.name' 2>/dev/null)
	is_zip=$(jsonfilter -i "$tmpinput" -e '@.is_zip' 2>/dev/null)
	rm -f "$tmpinput"

	name=$(echo "$name" | sed 's/[^a-zA-Z0-9_]/_/g; s/^_*//; s/_*$//')

	if [ -z "$name" ]; then
		json_error "Missing name"
		return
	fi

	local staging="/tmp/streamlit_upload_${name}.b64"
	if [ ! -s "$staging" ]; then
		json_error "No upload data found for $name"
		return
	fi

	local data_path
	config_load "$CONFIG"
	config_get data_path main data_path "/srv/streamlit"
	mkdir -p "$data_path/apps"

	if [ "$is_zip" = "1" ] || [ "$is_zip" = "true" ]; then
		# Decode as ZIP and extract
		local tmpzip="/tmp/upload_${name}_$$.zip"
		base64 -d < "$staging" > "$tmpzip" 2>/dev/null
		rm -f "$staging"

		if [ ! -s "$tmpzip" ]; then
			rm -f "$tmpzip"
			json_error "Failed to decode ZIP"
			return
		fi

		local app_dir="$data_path/apps/$name"
		mkdir -p "$app_dir"
		unzip -o "$tmpzip" -d "$app_dir" >/dev/null 2>&1
		rm -f "$tmpzip"

		local main_py
		main_py=$(find "$app_dir" -maxdepth 2 -name "*.py" -type f | head -1)
		if [ -n "$main_py" ]; then
			# Install requirements if found (requirements.txt or requirements*.txt)
			if lxc_running; then
				local req_file=""
				if [ -f "$app_dir/requirements.txt" ]; then
					req_file="requirements.txt"
				else
					req_file=$(ls -1 "$app_dir"/requirements*.txt 2>/dev/null | head -1 | xargs basename 2>/dev/null)
				fi
				if [ -n "$req_file" ]; then
					lxc-attach -n "$LXC_NAME" -- pip3 install --break-system-packages -r "/srv/apps/${name}/${req_file}" >/dev/null 2>&1 &
				fi
			fi
			uci set "${CONFIG}.${name}=app"
			uci set "${CONFIG}.${name}.name=$name"
			uci set "${CONFIG}.${name}.path=$main_py"
			uci set "${CONFIG}.${name}.enabled=1"
			uci commit "$CONFIG"
			json_success "ZIP app deployed: $name"
		else
			json_error "No Python files found in archive"
		fi
	else
		# Decode as .py file
		local app_file="$data_path/apps/${name}.py"
		base64 -d < "$staging" > "$app_file" 2>/dev/null
		local rc=$?
		rm -f "$staging"

		if [ $rc -eq 0 ] && [ -s "$app_file" ]; then
			uci set "${CONFIG}.${name}=app"
			uci set "${CONFIG}.${name}.name=$name"
			uci set "${CONFIG}.${name}.path=${name}.py"
			uci set "${CONFIG}.${name}.enabled=1"
			uci commit "$CONFIG"
			json_success "App uploaded: $name"
		else
			rm -f "$app_file"
			json_error "Failed to decode app content"
		fi
	fi
}

# List instances
list_instances() {
	json_init_obj
	json_add_array "instances"

	config_load "$CONFIG"

	_add_instance_json() {
		local section="$1"
		local name app port enabled autostart inst_name

		config_get inst_name "$section" name ""
		config_get app "$section" app ""
		config_get port "$section" port ""
		config_get enabled "$section" enabled "0"
		config_get autostart "$section" autostart "0"

		[ -z "$app" ] && return

		json_add_object ""
		json_add_string "id" "$section"
		json_add_string "name" "$inst_name"
		json_add_string "app" "$app"
		json_add_int "port" "$port"
		json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )"
		json_add_boolean "autostart" "$( [ "$autostart" = "1" ] && echo 1 || echo 0 )"
		json_close_object
	}

	config_foreach _add_instance_json instance

	json_close_array
	json_close_obj
}

# Add instance
add_instance() {
	read -r input
	local id name app port
	id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null)
	name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
	app=$(echo "$input" | jsonfilter -e '@.app' 2>/dev/null)
	port=$(echo "$input" | jsonfilter -e '@.port' 2>/dev/null)

	if [ -z "$id" ] || [ -z "$app" ] || [ -z "$port" ]; then
		json_error "Missing id, app, or port"
		return
	fi

	[ -z "$name" ] && name="$id"

	# Validate port number
	if ! echo "$port" | grep -qE '^[0-9]+$'; then
		json_error "Invalid port number"
		return
	fi

	# Check if instance already exists
	local existing
	existing=$(uci -q get "${CONFIG}.${id}")
	if [ -n "$existing" ]; then
		json_error "Instance $id already exists"
		return
	fi

	uci set "${CONFIG}.${id}=instance"
	uci set "${CONFIG}.${id}.name=$name"
	uci set "${CONFIG}.${id}.app=$app"
	uci set "${CONFIG}.${id}.port=$port"
	uci set "${CONFIG}.${id}.enabled=1"
	uci set "${CONFIG}.${id}.autostart=1"
	uci commit "$CONFIG"

	json_success "Instance added: $id"
}

# Remove instance
remove_instance() {
	read -r input
	local id
	id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null)

	if [ -z "$id" ]; then
		json_error "Missing instance id"
		return
	fi

	# Check if instance exists
	local existing
	existing=$(uci -q get "${CONFIG}.${id}")
	if [ -z "$existing" ]; then
		json_error "Instance $id not found"
		return
	fi

	uci delete "${CONFIG}.${id}"
	uci commit "$CONFIG"

	json_success "Instance removed: $id"
}

# Rename app
rename_app() {
	read -r input
	local id name
	id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null)
	name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)

	if [ -z "$id" ] || [ -z "$name" ]; then
		json_error "Missing id or name"
		return
	fi

	# Create UCI section if it doesn't exist yet
	local existing
	existing=$(uci -q get "${CONFIG}.${id}")
	if [ -z "$existing" ]; then
		uci set "${CONFIG}.${id}=app"
		uci set "${CONFIG}.${id}.enabled=1"
	fi

	uci set "${CONFIG}.${id}.name=$name"
	uci commit "$CONFIG"
	json_success "App renamed"
}

# Rename instance
rename_instance() {
	read -r input
	local id name
	id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null)
	name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)

	if [ -z "$id" ] || [ -z "$name" ]; then
		json_error "Missing id or name"
		return
	fi

	local existing
	existing=$(uci -q get "${CONFIG}.${id}")
	if [ -z "$existing" ]; then
		json_error "Instance $id not found"
		return
	fi

	uci set "${CONFIG}.${id}.name=$name"
	uci commit "$CONFIG"
	json_success "Instance renamed"
}

# Enable instance
enable_instance() {
	read -r input
	local id
	id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null)

	if [ -z "$id" ]; then
		json_error "Missing instance id"
		return
	fi

	uci set "${CONFIG}.${id}.enabled=1"
	uci commit "$CONFIG"

	json_success "Instance enabled: $id"
}

# Disable instance
disable_instance() {
	read -r input
	local id
	id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null)

	if [ -z "$id" ]; then
		json_error "Missing instance id"
		return
	fi

	uci set "${CONFIG}.${id}.enabled=0"
	uci commit "$CONFIG"

	json_success "Instance disabled: $id"
}

# Preview ZIP contents
preview_zip() {
	# Write stdin to temp file to avoid shell variable size limits
	local tmpinput="/tmp/rpcd_preview_$$.json"
	cat > "$tmpinput"

	local tmpzip="/tmp/preview_$$.zip"
	jsonfilter -i "$tmpinput" -e '@.content' 2>/dev/null | base64 -d > "$tmpzip" 2>/dev/null
	rm -f "$tmpinput"

	if [ ! -s "$tmpzip" ]; then
		rm -f "$tmpzip"
		json_error "Failed to decode ZIP"
		return
	fi

	json_init_obj
	json_add_array "files"

	# Use unzip to list contents
	unzip -l "$tmpzip" 2>/dev/null | tail -n +4 | head -n -2 | while read -r size date time name; do
		[ -z "$name" ] && continue
		local is_dir=0
		echo "$name" | grep -q '/$' && is_dir=1

		json_add_object ""
		json_add_string "path" "$name"
		json_add_int "size" "$size"
		json_add_boolean "is_dir" "$is_dir"
		json_close_object
	done

	json_close_array
	json_close_obj

	rm -f "$tmpzip"
}

# Upload ZIP with selected files
upload_zip() {
	# Write stdin to temp file to avoid shell variable size limits
	local tmpinput="/tmp/rpcd_zipinput_$$.json"
	cat > "$tmpinput"

	local name selected_files
	name=$(jsonfilter -i "$tmpinput" -e '@.name' 2>/dev/null)
	selected_files=$(jsonfilter -i "$tmpinput" -e '@.selected_files' 2>/dev/null)

	# Sanitize name for UCI compatibility (alphanumeric and underscores only)
	name=$(echo "$name" | sed 's/[^a-zA-Z0-9_]/_/g; s/^_*//; s/_*$//')

	if [ -z "$name" ]; then
		rm -f "$tmpinput"
		json_error "Missing name"
		return
	fi

	local data_path
	config_load "$CONFIG"
	config_get data_path main data_path "/srv/streamlit"

	local app_dir="$data_path/apps/$name"
	local tmpzip="/tmp/upload_$$.zip"

	# Extract base64 content and decode directly to zip file
	jsonfilter -i "$tmpinput" -e '@.content' 2>/dev/null | base64 -d > "$tmpzip" 2>/dev/null
	rm -f "$tmpinput"

	if [ ! -s "$tmpzip" ]; then
		rm -f "$tmpzip"
		json_error "Failed to decode ZIP"
		return
	fi

	mkdir -p "$app_dir"

	# Extract selected files or all if none specified
	local file_count=$(echo "$selected_files" | jsonfilter -e '@[*]' 2>/dev/null | wc -l)
	if [ -n "$selected_files" ] && [ "$file_count" -gt 0 ] 2>/dev/null; then
		# Parse selected files array and extract each
		echo "$selected_files" | jsonfilter -e '@[*]' 2>/dev/null | while read -r filepath; do
			[ -z "$filepath" ] && continue
			unzip -o "$tmpzip" "$filepath" -d "$app_dir" >/dev/null 2>&1
		done
	else
		# Extract all
		unzip -o "$tmpzip" -d "$app_dir" >/dev/null 2>&1
	fi

	rm -f "$tmpzip"

	# Find main .py file for registration
	local main_py
	main_py=$(find "$app_dir" -maxdepth 2 -name "*.py" -type f | head -1)

	if [ -n "$main_py" ]; then
		# Install requirements if found (requirements.txt or requirements*.txt)
		if lxc_running; then
			local req_file=""
			if [ -f "$app_dir/requirements.txt" ]; then
				req_file="requirements.txt"
			else
				req_file=$(ls -1 "$app_dir"/requirements*.txt 2>/dev/null | head -1 | xargs basename 2>/dev/null)
			fi
			if [ -n "$req_file" ]; then
				lxc-attach -n "$LXC_NAME" -- pip3 install --break-system-packages -r "/srv/apps/${name}/${req_file}" >/dev/null 2>&1 &
			fi
		fi

		# Register in UCI
		uci set "${CONFIG}.${name}=app"
		uci set "${CONFIG}.${name}.name=$name"
		uci set "${CONFIG}.${name}.path=$main_py"
		uci set "${CONFIG}.${name}.enabled=1"
		uci commit "$CONFIG"

		json_init_obj
		json_add_boolean "success" 1
		json_add_string "message" "App deployed: $name"
		json_add_string "path" "$app_dir"
		json_add_string "main_file" "$main_py"
		json_close_obj
	else
		json_error "No Python files found in extracted archive"
	fi
}

# Get Gitea config
get_gitea_config() {
	config_load "$CONFIG"
	local enabled url user token

	config_get enabled gitea enabled "0"
	config_get url gitea url ""
	config_get user gitea user ""
	config_get token gitea token ""

	json_init_obj
	json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )"
	json_add_string "url" "$url"
	json_add_string "user" "$user"
	json_add_boolean "has_token" "$( [ -n "$token" ] && echo 1 || echo 0 )"
	json_close_obj
}

# Save Gitea config
save_gitea_config() {
	read -r input
	local enabled url user token

	enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null)
	url=$(echo "$input" | jsonfilter -e '@.url' 2>/dev/null)
	user=$(echo "$input" | jsonfilter -e '@.user' 2>/dev/null)
	token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null)

	# Ensure gitea section exists
	uci -q get "${CONFIG}.gitea" >/dev/null || uci set "${CONFIG}.gitea=gitea"

	[ -n "$enabled" ] && uci set "${CONFIG}.gitea.enabled=$enabled"
	[ -n "$url" ] && uci set "${CONFIG}.gitea.url=$url"
	[ -n "$user" ] && uci set "${CONFIG}.gitea.user=$user"
	[ -n "$token" ] && uci set "${CONFIG}.gitea.token=$token"

	uci commit "$CONFIG"

	json_success "Gitea configuration saved"
}

# Clone app from Gitea
gitea_clone() {
	read -r input
	local name repo

	name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
	repo=$(echo "$input" | jsonfilter -e '@.repo' 2>/dev/null)

	if [ -z "$name" ] || [ -z "$repo" ]; then
		json_error "Missing name or repo"
		return
	fi

	# Run clone in background
	/usr/sbin/streamlitctl gitea clone "$name" "$repo" >/var/log/streamlit-gitea.log 2>&1 &

	json_init_obj
	json_add_boolean "success" 1
	json_add_string "message" "Cloning $repo to $name in background"
	json_close_obj
}

# Pull app from Gitea
gitea_pull() {
	read -r input
	local name

	name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)

	if [ -z "$name" ]; then
		json_error "Missing app name"
		return
	fi

	# Run pull
	/usr/sbin/streamlitctl gitea pull "$name" >/var/log/streamlit-gitea.log 2>&1

	if [ $? -eq 0 ]; then
		json_success "App updated from Gitea: $name"
	else
		json_error "Failed to pull app from Gitea"
	fi
}

# List Gitea repositories
gitea_list_repos() {
	config_load "$CONFIG"
	local enabled url user token

	config_get enabled gitea enabled "0"
	config_get url gitea url ""
	config_get user gitea user ""
	config_get token gitea token ""

	if [ "$enabled" != "1" ] || [ -z "$url" ] || [ -z "$token" ]; then
		json_error "Gitea not configured"
		return
	fi

	# Call Gitea API to list user repos
	local api_url="${url}/api/v1/user/repos"
	local response

	response=$(curl -s -H "Authorization: token $token" "$api_url" 2>/dev/null)

	if [ -z "$response" ]; then
		json_error "Failed to connect to Gitea"
		return
	fi

	json_init_obj
	json_add_array "repos"

	# Parse JSON response (simple extraction)
	echo "$response" | jsonfilter -e '@[*].full_name' 2>/dev/null | while read -r repo; do
		[ -z "$repo" ] && continue
		json_add_string "" "$repo"
	done

	json_close_array
	json_close_obj
}

# Check install progress
get_install_progress() {
	local log_file="/var/log/streamlit-install.log"
	local status="unknown"
	local progress=0
	local message=""

	if [ -f "$log_file" ]; then
		# Check for completion markers
		if grep -q "Installation complete" "$log_file" 2>/dev/null; then
			status="completed"
			progress=100
			message="Installation completed successfully"
		elif grep -q "ERROR" "$log_file" 2>/dev/null; then
			status="error"
			message=$(grep "ERROR" "$log_file" | tail -1)
		else
			status="running"
			# Estimate progress based on log content
			if grep -q "Rootfs created" "$log_file" 2>/dev/null; then
				progress=80
				message="Setting up container..."
			elif grep -q "Extracting rootfs" "$log_file" 2>/dev/null; then
				progress=60
				message="Extracting container rootfs..."
			elif grep -q "Downloading Alpine" "$log_file" 2>/dev/null; then
				progress=40
				message="Downloading Alpine rootfs..."
			elif grep -q "Installing Streamlit" "$log_file" 2>/dev/null; then
				progress=20
				message="Starting installation..."
			else
				progress=10
				message="Initializing..."
			fi
		fi
	else
		status="not_started"
		message="Installation has not been started"
	fi

	# Check if process is still running
	if pgrep -f "streamlitctl install" >/dev/null 2>&1; then
		status="running"
	fi

	json_init_obj
	json_add_string "status" "$status"
	json_add_int "progress" "$progress"
	json_add_string "message" "$message"
	json_close_obj
}

# Main RPC handler
case "$1" in
	list)
		cat <<-EOF
		{
			"get_status": {},
			"get_config": {},
			"save_config": {"http_port": 8501, "http_host": "str", "data_path": "str", "memory_limit": "str", "enabled": "str", "active_app": "str", "headless": "str", "browser_gather_usage_stats": "str", "theme_base": "str", "theme_primary_color": "str"},
			"start": {},
			"stop": {},
			"restart": {},
			"install": {},
			"uninstall": {},
			"update": {},
			"get_logs": {"lines": 100},
			"list_apps": {},
			"get_app": {"name": "str"},
			"add_app": {"name": "str", "path": "str"},
			"remove_app": {"name": "str"},
			"set_active_app": {"name": "str"},
			"upload_app": {"name": "str", "content": "str"},
			"upload_chunk": {"name": "str", "data": "str", "index": 0},
			"upload_finalize": {"name": "str", "is_zip": "str"},
			"preview_zip": {"content": "str"},
			"upload_zip": {"name": "str", "content": "str", "selected_files": []},
			"get_install_progress": {},
			"list_instances": {},
			"add_instance": {"id": "str", "name": "str", "app": "str", "port": 8501},
			"remove_instance": {"id": "str"},
			"enable_instance": {"id": "str"},
			"disable_instance": {"id": "str"},
			"rename_app": {"id": "str", "name": "str"},
			"rename_instance": {"id": "str", "name": "str"},
			"get_gitea_config": {},
			"save_gitea_config": {"enabled": "str", "url": "str", "user": "str", "token": "str"},
			"gitea_clone": {"name": "str", "repo": "str"},
			"gitea_pull": {"name": "str"},
			"gitea_list_repos": {}
		}
		EOF
		;;
	call)
		case "$2" in
			get_status)
				get_status
				;;
			get_config)
				get_config
				;;
			save_config)
				save_config
				;;
			start)
				start_service
				;;
			stop)
				stop_service
				;;
			restart)
				restart_service
				;;
			install)
				install
				;;
			uninstall)
				uninstall
				;;
			update)
				update
				;;
			get_logs)
				get_logs
				;;
			list_apps)
				list_apps
				;;
			get_app)
				get_app
				;;
			add_app)
				add_app
				;;
			remove_app)
				remove_app
				;;
			set_active_app)
				set_active_app
				;;
			upload_app)
				upload_app
				;;
			upload_chunk)
				upload_chunk
				;;
			upload_finalize)
				upload_finalize
				;;
			preview_zip)
				preview_zip
				;;
			upload_zip)
				upload_zip
				;;
			get_install_progress)
				get_install_progress
				;;
			list_instances)
				list_instances
				;;
			add_instance)
				add_instance
				;;
			remove_instance)
				remove_instance
				;;
			enable_instance)
				enable_instance
				;;
			disable_instance)
				disable_instance
				;;
			rename_app)
				rename_app
				;;
			rename_instance)
				rename_instance
				;;
			get_gitea_config)
				get_gitea_config
				;;
			save_gitea_config)
				save_gitea_config
				;;
			gitea_clone)
				gitea_clone
				;;
			gitea_pull)
				gitea_pull
				;;
			gitea_list_repos)
				gitea_list_repos
				;;
			*)
				json_error "Unknown method: $2"
				;;
		esac
		;;
esac
