#!/usr/bin/env sh
# install.sh — install the latest release of Skald Circle
#
# Usage:
#   curl -fsSL https://builds.skaldagent.net/install.sh | bash
#
# Supports Linux (systemd) and macOS ARM64 (launchd).
# Default install dir: ~/.local/share/skald-circle (override with SKALD_DIR).
#
# If Docker is missing, the installer can optionally install it.

set -eu

# ── User overrides ────────────────────────────────────────────────────────────
INSTALL_DIR="${SKALD_DIR:-$HOME/.local/share/skald-circle}"

# ── Detect interactive stdin ──────────────────────────────────────────────────
# We need this early: the confirmation prompt reads from /dev/tty when stdin is
# piped (curl | bash), but we only show it when a terminal is actually available.
if [ -t 0 ]; then
    IS_INTERACTIVE=true
else
    IS_INTERACTIVE=false
fi

# ── Colours (if terminal) ─────────────────────────────────────────────────────
if [ -t 1 ]; then
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    YELLOW='\033[1;33m'
    CYAN='\033[0;36m'
    BOLD='\033[1m'
    NC='\033[0m'
else
    RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; NC=''
fi

info()  { printf "${GREEN}%s${NC}\n" "$*"; }
warn()  { printf "${YELLOW}⚠ %s${NC}\n" "$*"; }
err()   { printf "${RED}✖ %s${NC}\n" "$*"; }
header(){ printf "\n${BOLD}%s${NC}\n" "$*"; }
banner(){ printf "\n${CYAN}${BOLD}%s${NC}\n" "$*"; }
# ── Network IP helper ──────────────────────────────────────────────────────────
# Returns the primary non-loopback network IP, or empty string if unavailable.
get_network_ip() {
    if command -v ip >/dev/null 2>&1; then
        ip route get 1 2>/dev/null | awk '{print $7}'
    elif command -v ifconfig >/dev/null 2>&1; then
        ifconfig 2>/dev/null | grep -E 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | head -1
    elif command -v hostname >/dev/null 2>&1; then
        hostname -I 2>/dev/null | awk '{print $1}'
    fi
}



# ── Prompt helpers (work from piped stdin too) ────────────────────────────────
# Both read from /dev/tty when stdin is piped (curl | bash), so the user can
# always see and answer.

prompt_enter() {
    local msg="${1:-Press Enter to continue or Ctrl+C to cancel}"
    if [ "$IS_INTERACTIVE" = true ]; then
        printf "%s" "$msg" >&2
        read -r _ || true
    elif (: </dev/tty) 2>/dev/null; then
        printf "%s" "$msg" >/dev/tty
        IFS= read -r _ </dev/tty || true
    fi
}

prompt_yes_no() {
    local msg="${1:-Continue? [Y/n]}"
    local ans
    if [ "$IS_INTERACTIVE" = true ]; then
        printf "%s" "$msg" >&2
        read -r ans || ans="y"
    elif (: </dev/tty) 2>/dev/null; then
        printf "%s" "$msg" >/dev/tty
        IFS= read -r ans </dev/tty || ans="y"
    else
        ans="n"
    fi
    case "$(printf "%s" "$ans" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" in
        ""|"y"|"yes") return 0 ;;
        *) return 1 ;;
    esac
}

# Like prompt_yes_no but defaults to NO — for destructive confirmations.
prompt_yes_no_default_no() {
    local msg="${1:-Continue? [y/N]}"
    local ans=""
    if [ "$IS_INTERACTIVE" = true ]; then
        printf "%s" "$msg" >&2
        read -r ans || ans=""
    elif (: </dev/tty) 2>/dev/null; then
        printf "%s" "$msg" >/dev/tty
        IFS= read -r ans </dev/tty || ans=""
    fi
    case "$(printf "%s" "$ans" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" in
        "y"|"yes") return 0 ;;
        *) return 1 ;;
    esac
}

# ── Stop a running instance (for reinstall over an existing install) ──────────
# Stops the service and waits, bounded, for the process to exit — so the
# extraction never overwrites a live binary (ETXTBSY on Linux).
stop_existing_service() {
    case "$OS" in
        linux)
            command -v systemctl >/dev/null 2>&1 && \
                systemctl --user stop skald-circle.service 2>/dev/null || true
            ;;
        darwin)
            command -v launchctl >/dev/null 2>&1 && \
                launchctl unload "$HOME/Library/LaunchAgents/com.skald.circle.plist" 2>/dev/null || true
            ;;
    esac
    if command -v pgrep >/dev/null 2>&1; then
        local i=0
        while pgrep -f "${INSTALL_DIR}/bin/skald" >/dev/null 2>&1; do
            i=$((i + 1))
            [ "$i" -gt 20 ] && break
            sleep 1
        done
    else
        sleep 2
    fi
}

# ── systemd user lingering ────────────────────────────────────────────────────
# A `systemctl --user` unit runs under the per-user manager (user@UID.service),
# which systemd starts at first login and STOPS when the user's last session
# ends — taking every user service down with it. So without lingering the server
# dies the moment you close the SSH session that started it, and never comes up
# at boot. Enabling it is the whole difference between "runs while I'm logged
# in" and "is a daemon".
enable_linger() {
    local target="${USER:-$(id -un)}"

    if ! command -v loginctl >/dev/null 2>&1; then
        warn "loginctl not found — cannot enable lingering."
        echo "  The server will stop when you log out of this machine."
        return 0
    fi

    case "$(loginctl show-user "$target" --property=Linger 2>/dev/null || true)" in
        *=yes) info "✔ Lingering already enabled for ${target}"; return 0 ;;
    esac

    # Enabling linger for yourself is normally allowed without elevation; fall
    # back to sudo, non-interactive first so `curl | bash` never blocks on a
    # password prompt it has no terminal to answer.
    if loginctl enable-linger "$target" 2>/dev/null \
        || sudo -n loginctl enable-linger "$target" 2>/dev/null \
        || { [ "$IS_INTERACTIVE" = true ] && sudo loginctl enable-linger "$target"; }; then
        info "✔ Lingering enabled — the server keeps running after you log out"
    else
        warn "Could not enable lingering for ${target}."
        echo "  Without it, the server stops as soon as your last session ends"
        echo "  and does not start at boot. Run this once, as an administrator:"
        echo ""
        echo "      sudo loginctl enable-linger ${target}"
        echo ""
    fi
}

# ── Docker install helper ─────────────────────────────────────────────────────
install_docker() {
    if [ "$OS" = "linux" ]; then
        info "▶ Installing Docker via get.docker.com …"
        curl -fsSL https://get.docker.com | sh
        info "✔ Docker installed"

        # Add current user to docker group so they don't need sudo for every command
        if command -v usermod >/dev/null 2>&1; then
            sudo usermod -aG docker "$USER"
            warn "You may need to log out and back in for the docker group to take effect."
        fi
        info "✔ User added to docker group"
    elif [ "$OS" = "darwin" ]; then
        if command -v brew >/dev/null 2>&1; then
            info "▶ Installing Docker Desktop via Homebrew …"
            brew install --cask docker
            info "✔ Docker Desktop installed. Open it from Applications to complete setup."
        else
            err "Homebrew not found. Please install Docker Desktop manually from:"
            err "  https://docs.docker.com/desktop/setup/install/mac-install/"
            err "Then re-run this installer."
            exit 1
        fi
    fi
}

ask_install_docker() {
    echo ""
    header "🐳 Docker"
    echo ""
    if command -v docker >/dev/null 2>&1 && docker version >/dev/null 2>&1; then
        info "✔ Docker is installed and the daemon is running."
        return 0
    elif command -v docker >/dev/null 2>&1; then
        echo "  Docker CLI is present but the daemon is not running."
        echo "  Please start Docker before starting the server."
        echo ""
        return 0
    else
        warn "Docker is required but not found."
        echo ""
        echo "  Skald uses Docker to run sandboxed user containers."
        echo "  The server will not start without it."
        echo ""
        if prompt_yes_no "  Install Docker now? [Y/n] "; then
            install_docker
            echo ""
        else
            warn "Skipping Docker installation."
            echo "  You can install it later: https://docs.docker.com/engine/install/"
            echo ""
        fi
    fi
}


# ── Optional dependency checks ────────────────────────────────────────────────
# These are just warnings — the server starts without them, but some MCP servers
# or plugins won't work.

check_optional_deps() {
    echo ""
    header "🔧 Optional dependencies"
    echo ""

    if command -v python3 >/dev/null 2>&1; then
        info "✔ Python 3 found ($(python3 --version 2>&1 | head -1))"
    else
        warn "Python 3 not found — the TTS plugins and host-run connectors will not work."
        echo "  Install it from https://www.python.org/downloads/"
        echo ""
    fi

    if command -v node >/dev/null 2>&1; then
        info "✔ Node.js found ($(node --version 2>&1 | head -1))"
    else
        warn "Node.js not found — WhatsApp MCP server will not work."
        echo "  Install it from https://nodejs.org/ (version 18 or later)"
        echo ""
    fi
}
# ── Platform detection ────────────────────────────────────────────────────────
OS="$(uname -s)"
ARCH="$(uname -m)"

case "$OS" in
    Linux)  OS="linux"  ;;
    Darwin) OS="darwin" ;;
    *)      err "Unsupported OS: $OS"; exit 1 ;;
esac

case "$ARCH" in
    x86_64)
        ARCH="amd64"
        if [ "$OS" = "darwin" ]; then
            err "Intel Macs are not supported. Apple Silicon (M1+) only."
            exit 1
        fi
        ;;
    aarch64|arm64)
        ARCH="arm64"
        ;;
    *)      err "Unsupported architecture: $ARCH"; exit 1 ;;
esac

# ── Dependency checks ─────────────────────────────────────────────────────────
command -v curl >/dev/null 2>&1 || { err "curl is required but not installed."; exit 1; }

if [ "$OS" = "linux" ]; then
    command -v systemctl >/dev/null 2>&1 || { warn "systemd not found — service will not be installed automatically."; NOSYSTEMD=1; }
elif [ "$OS" = "darwin" ]; then
    command -v launchctl >/dev/null 2>&1 || { err "launchctl not found."; exit 1; }
fi

# ── Banner + fetch latest version ─────────────────────────────────────────────
banner "╔══════════════════════════════════════════╗"
banner "║        Skald Circle — Installer          ║"
banner "╚══════════════════════════════════════════╝"
echo ""

info "🔍 Looking up latest release …"
BASE_URL="https://builds.skaldagent.net"
LATEST_URL="${BASE_URL}/releases/LATEST"
VERSION="$(curl -fsSL "$LATEST_URL" | head -1 | tr -d '[:space:]')"

if [ -z "$VERSION" ]; then
    err "Could not determine latest release version."
    err "Check ${LATEST_URL} or try install-nightly.sh for the latest build."
    exit 1
fi

TARBALL_URL="${BASE_URL}/releases/${VERSION}/skald-circle-${VERSION}-${OS}-${ARCH}.tar.gz"

# ── Summary + confirmation ────────────────────────────────────────────────────
echo ""
header "Installation summary"
echo ""
echo "  What       : Skald Circle ${VERSION}"
echo "  Platform   : ${OS}/${ARCH}"
echo "  Install to : ${INSTALL_DIR}"
echo "  Service    : $( [ "$OS" = "linux" ] && echo "systemd (user)" || echo "launchd" )"
echo ""
echo "  The installer will check for Docker and offer to install it if missing."
echo "  Python 3 and Node.js are optional — needed for some MCP servers."
echo ""

prompt_enter "Press Enter to continue or Ctrl+C to cancel "

# ── Docker check & install ────────────────────────────────────────────────────
ask_install_docker

# ── Optional dependency check ─────────────────────────────────────────────────
check_optional_deps

# ── Existing installation? ────────────────────────────────────────────────────
# This installer targets a fresh install; the supported in-place upgrade path is
# update.sh (keeps your data, restarts the service safely). If an install is
# already here, point the user there and only reinstall over it on request.
if [ -x "$INSTALL_DIR/bin/skald" ]; then
    echo ""
    warn "An existing installation was found at ${INSTALL_DIR}."
    echo "  To upgrade in place (keeping your database and config), use:"
    echo "    ${INSTALL_DIR}/update.sh"
    echo ""
    if prompt_yes_no_default_no "  Reinstall over it instead? Data is kept, the service restarts. [y/N] "; then
        info "⏹️  Stopping the running instance before reinstalling …"
        stop_existing_service
    else
        info "Aborted — run ${INSTALL_DIR}/update.sh to upgrade."
        exit 0
    fi
    echo ""
fi

# ── Download & extract ────────────────────────────────────────────────────────
# Download to a temp file and verify the archive BEFORE touching the install dir
# — the same ordering update.sh uses, and for the same reason. Piping curl
# straight into tar half-extracts a truncated download, which on the
# reinstall-over-an-existing-install path above leaves a tree mixing old and new
# files: worse than either version, and with no error to say so.
info "↓ Downloading Skald Circle ${VERSION} …"

TMP_TARBALL="$(mktemp -t skald-install.XXXXXX.tar.gz)"
STAGING="$(mktemp -d -t skald-install-staging.XXXXXX)"
trap 'rm -f "$TMP_TARBALL" 2>/dev/null || true; rm -rf "$STAGING" 2>/dev/null || true' EXIT

curl -fsSL -o "$TMP_TARBALL" "$TARBALL_URL"

info "🔎 Verifying archive …"
tar xzf "$TMP_TARBALL" -C "$STAGING" --strip-components=1
if [ ! -x "$STAGING/bin/skald" ]; then
    err "Downloaded archive is invalid — skald binary not found."
    err "Nothing was written to ${INSTALL_DIR}."
    exit 1
fi

mkdir -p "$INSTALL_DIR"
tar xzf "$TMP_TARBALL" -C "$INSTALL_DIR" --strip-components=1

if [ ! -x "$INSTALL_DIR/bin/skald" ]; then
    err "Extraction failed — skald binary not found."
    exit 1
fi

info "✔ Extracted to ${INSTALL_DIR}"

# ── Python venv (best-effort) ─────────────────────────────────────────────────
# Create the venv inline instead of calling run.sh (which also launches the
# server and would hang the installer).
info "🔧 Setting up Python virtual environment …"
VENV_DIR="${INSTALL_DIR}/.venv"
REQUIREMENTS="${INSTALL_DIR}/requirements.txt"

# A venv is usable only if python3 AND pip both work. Ubuntu's `python3 -m venv`
# without the python3-venv package leaves a venv with python3 but no pip — detect
# that and recreate, so a broken venv never survives a restart.
if [ ! -f "$VENV_DIR/bin/python3" ] || ! "$VENV_DIR/bin/python3" -m pip --version >/dev/null 2>&1; then
    rm -rf "$VENV_DIR"
    if command -v uv >/dev/null 2>&1; then
        uv venv --seed "$VENV_DIR" && uv pip install -r "$REQUIREMENTS" \
            && info "✔ Python venv ready (uv)" \
            || warn "Python venv setup failed — the TTS plugins and host-run connectors will be unavailable."
    elif command -v python3 >/dev/null 2>&1; then
        python3 -m venv "$VENV_DIR" && "$VENV_DIR/bin/pip" install -r "$REQUIREMENTS" \
            && info "✔ Python venv ready (pip)" \
            || warn "Python venv setup failed — the TTS plugins and host-run connectors will be unavailable."
    else
        warn "python3 not found — the TTS plugins and host-run connectors will be unavailable."
    fi
else
    info "✔ Python venv already exists"
fi

# ── Install daemon ────────────────────────────────────────────────────────────
if [ "$OS" = "linux" ] && [ -z "${NOSYSTEMD:-}" ]; then
    header "⚡ Installing systemd user service …"

    mkdir -p "$HOME/.config/systemd/user"

    cat > "$HOME/.config/systemd/user/skald-circle.service" <<- SERVICE
[Unit]
Description=Skald Circle (release ${VERSION})
Documentation=https://skaldagent.net
# No After=docker.service here: this is a *user* unit, and docker.service is a
# system unit the user manager knows nothing about — the dependency would be
# silently ignored. Docker may therefore still be starting when we do; the
# server fails fast when the daemon is unreachable and Restart brings it back a
# few seconds later, so boot ordering settles itself.

[Service]
Type=simple
ExecStart=${INSTALL_DIR}/run.sh
WorkingDirectory=${INSTALL_DIR}
# always, not on-failure: run.sh exits 0 on any graceful shutdown, including one
# nobody asked for (a stray SIGTERM to the server), which on-failure would treat
# as a clean stop and leave the box down. An explicit "systemctl --user stop"
# is unaffected — systemd never restarts after a requested stop.
Restart=always
RestartSec=5
# The default soft limit is 1024, which one process shares between the HTTP
# listener, every user's SQLite handles and three pipes per connector process.
# Running out does not degrade gracefully: accept() starts failing with EMFILE
# and the whole app stops answering while still looking healthy from outside.
LimitNOFILE=65536
Environment=SKALD_BIN=${INSTALL_DIR}/bin/skald
Environment=SKALD_SETUP_BIN=${INSTALL_DIR}/bin/skald-setup

[Install]
WantedBy=default.target
SERVICE

    systemctl --user daemon-reload
    systemctl --user enable --now skald-circle.service

    info "✔ Service installed and started"

    enable_linger

    echo ""
    echo "  Status:  systemctl --user status skald-circle"
    echo "  Logs:    journalctl --user -u skald-circle -f"

elif [ "$OS" = "darwin" ]; then
    header "⚡ Installing launchd agent …"

    mkdir -p "$HOME/Library/LaunchAgents" "$INSTALL_DIR/logs"

    PLIST="$HOME/Library/LaunchAgents/com.skald.circle.plist"

    cat > "$PLIST" <<- 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>com.skald.circle</string>

    <key>ProgramArguments</key>
    <array>
        <string>${INSTALL_DIR}/run.sh</string>
    </array>

    <key>WorkingDirectory</key>
    <string>${INSTALL_DIR}</string>

    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>

    <key>StandardOutPath</key>
    <string>${INSTALL_DIR}/logs/stdout.log</string>
    <key>StandardErrorPath</key>
    <string>${INSTALL_DIR}/logs/stderr.log</string>

    <key>EnvironmentVariables</key>
    <dict>
        <key>SKALD_BIN</key>
        <string>${INSTALL_DIR}/bin/skald</string>
        <key>SKALD_SETUP_BIN</key>
        <string>${INSTALL_DIR}/bin/skald-setup</string>
    </dict>
</dict>
</plist>
PLIST

    # Idempotent: unload any previously-loaded agent first, so a re-install
    # reloads the freshly written plist instead of failing on "already loaded".
    launchctl unload "$PLIST" 2>/dev/null || true
    launchctl load "$PLIST"

    info "✔ Agent installed and started"
    echo ""
    echo "  Status:  launchctl list com.skald.circle"
    echo "  Logs:    tail -f ${INSTALL_DIR}/logs/stdout.log"

elif [ -n "${NOSYSTEMD:-}" ]; then
    warn "systemd not available — start manually: ${INSTALL_DIR}/run.sh"
fi

# ── Welcome + first-run setup ─────────────────────────────────────────────────
echo ""
header "👋 Welcome to Skald Circle!"

if [ -x "$INSTALL_DIR/bin/skald-setup" ]; then
    echo ""
    echo "  The server is running. Now let's create your admin account."
    echo "  You'll be asked for a username and password."
    echo ""
    # skald-setup uses the relative path `database/system.db`, so we cd to the
    # install directory first. When running via curl | bash the cwd is ~, which
    # would create `~/database/system.db` instead of the correct location.
    cd "$INSTALL_DIR"
    if [ "$IS_INTERACTIVE" = true ]; then
        "bin/skald-setup"
    elif (: </dev/tty) 2>/dev/null; then
        "bin/skald-setup" </dev/tty
    else
        echo "  From a terminal, run:"
        echo "    cd ${INSTALL_DIR} && ./bin/skald-setup"
    fi
    echo ""
fi

# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
info "✅ Skald Circle ${VERSION} installed successfully!"
echo ""

# Write channel tag and version for future updates
echo "release" > "$INSTALL_DIR/.release-channel"
echo "$VERSION" > "$INSTALL_DIR/.release-version"

# ── Hint about optional deps ──────────────────────────────────────────────────
if [ -f "${INSTALL_DIR}/requirements-optional.txt" ]; then
    echo ""
    info "💡 Optional GPU/ML dependencies available:"
    info "   ${INSTALL_DIR}/requirements-optional.txt"
    echo "   Install them manually if you use the Orpheus TTS plugin:"
    echo "     cd ${INSTALL_DIR} && .venv/bin/pip install -r requirements-optional.txt"
fi

echo "  Server : ${INSTALL_DIR}/run.sh"
echo "  Binary : ${INSTALL_DIR}/bin/skald"
echo "  Setup  : ${INSTALL_DIR}/bin/skald-setup"
NET_IP="$(get_network_ip)"
ADMIN_URL="${NET_IP:+http://${NET_IP}:9000}"
echo "  Admin console: ${ADMIN_URL:-http://localhost:9000}  (on the machine, or use the IP above from another device)"
echo "  Server IP:    ${NET_IP:-localhost}  (network address if available)"
echo ""
echo ""
echo "  Start:  $( [ "$OS" = "linux" ] && echo "systemctl --user start skald-circle" || echo "launchctl start com.skald.circle" )"
echo "  Stop:   $( [ "$OS" = "linux" ] && echo "systemctl --user stop skald-circle" || echo "launchctl stop com.skald.circle" )"
echo "  Logs:   $( [ "$OS" = "linux" ] && echo "journalctl --user -u skald-circle -f" || echo "tail -f ${INSTALL_DIR}/logs/stdout.log" )"
echo ""
