#!/bin/bash
# Run this from a terminal:
#   chmod +x install-catchupgist-linux.sh && ./install-catchupgist-linux.sh
# Some file managers (GNOME Files, some KDE Dolphin setups) will also
# run this directly if you mark it executable via Properties ->
# Permissions, then open it and choose "Run" when prompted -- that
# varies by desktop environment, so the terminal command above always
# works no matter what you're running.

set -e

# "Press Enter to close this window", but only when a human is there to
# press it. Piped or scripted runs (`curl ... | bash`, CI, a fleet tool)
# have no TTY, so the read hits EOF immediately and returns non-zero --
# which, as the last statement of the script, made a *successful*
# install exit 1. Never let this affect the exit status.
pause() {
    if [ -t 0 ]; then
        read -r -p "Press Enter to close this window..." _ || true
    fi
    return 0
}

# Is something listening on this port? Uses bash's own /dev/tcp rather
# than ss/lsof/netstat, because which of those exists varies by distro
# and macOS has a different set again -- and this has to work on both.
port_in_use() {
    ( exec 3<>"/dev/tcp/127.0.0.1/$1" ) >/dev/null 2>&1
}

# First free port at or after $1, giving up after 20 tries.
#
# Ticking rather than failing with "set CONSOLE_PORT and try again": most
# people installing this double-click a file, and telling them to set an
# environment variable is telling them to stop. It matters most on macOS,
# where AirPlay Receiver holds port 5000 by default (Monterey and later
# reserve 5000 and 7000 for it), so a fixed 5000 fails on a stock Mac
# through no fault of the user.
find_free_port() {
    p="$1"
    limit=$(( p + 20 ))
    while [ "$p" -lt "$limit" ]; do
        if ! port_in_use "$p"; then
            echo "$p"
            return 0
        fi
        p=$(( p + 1 ))
    done
    return 1
}
cd "$(dirname "$0")"

# Install into our own folder rather than wherever this file was
# downloaded to. Docker Compose names containers and the data volume
# after the folder it runs in, so two products sharing a folder means
# they fight over one volume -- and each install would overwrite the
# other's compose file. That applies to any future app that does the
# same thing, not just our two.
APP_DIR="catchupgist"
if [ -f "$APP_DIR/docker-compose.release.yml" ]; then
    # Re-run from the parent: step into the existing install.
    cd "$APP_DIR"
elif [ -f docker-compose.release.yml ] && grep -q "email-summary-pipeline" docker-compose.release.yml 2>/dev/null; then
    # Already installed right here, from before this script made its own
    # folder. Leave it where it is -- moving it would orphan the volume.
    :
else
    mkdir -p "$APP_DIR"
    cd "$APP_DIR"
fi

echo ""
echo "CatchUpGist - Setup"
echo "=============================="
echo ""

if ! command -v docker >/dev/null 2>&1; then
    echo "Docker isn't installed yet. Installing it now -- this only"
    echo "happens once. You may be asked for your sudo password."
    echo ""
    curl -fsSL https://get.docker.com | sudo sh
    if ! command -v docker >/dev/null 2>&1; then
        echo ""
        echo "Docker installation didn't complete. Install it yourself via"
        echo "your distribution's package manager, or see"
        echo "https://get.docker.com, then run this script again."
        echo ""
        pause
        exit 1
    fi
    echo ""
fi

if ! groups "$USER" | grep -qw docker; then
    echo "Adding $USER to the docker group so you don't need sudo for"
    echo "every command..."
    sudo usermod -aG docker "$USER"
    echo ""
    echo "=============================="
    echo "One-time step needed: log out and back in (or restart), then"
    echo "run this script again -- it'll pick up right where it left off."
    echo "=============================="
    echo ""
    pause
    exit 0
fi

if ! docker info >/dev/null 2>&1; then
    echo "Docker doesn't seem to be reachable yet."
    echo ""
    echo "If you were just added to the docker group or just installed"
    echo "Docker, log out and back in (or restart), then run this script"
    echo "again. Otherwise, check that the Docker service is running:"
    echo ""
    echo "    sudo systemctl start docker"
    echo ""
    pause
    exit 1
fi

echo "Docker is running. Setting up in this folder:"
echo "$(pwd)"
echo ""

# The console port. 5001 by default because CatchUpCast uses 5000, so
# someone running both would otherwise have the second one fail to start
# with a message about an address already in use.
if [ -f docker-compose.release.yml ]; then
    # Updating an existing install. Read the port back out of the compose
    # file rather than re-deriving it: someone who chose a different port
    # first time round should keep it, and the closing message must name
    # the right one.
    #
    # Deliberately NO "is the port free" check here -- on an update the
    # port is in use by our own container, so checking would refuse to
    # update every install that is currently running. That is exactly
    # what happened the first time this script was tested.
    CONSOLE_PORT="$(grep -oE '127\.0\.0\.1:[0-9]+:5000' docker-compose.release.yml | head -1 | cut -d: -f2)"
    # Host networking keeps it as CONSOLE_PORT=<port> instead (PRD 9).
    [ -n "$CONSOLE_PORT" ] || CONSOLE_PORT="$(grep -oE 'CONSOLE_PORT=[0-9]+' docker-compose.release.yml | head -1 | cut -d= -f2)"
    [ -n "$CONSOLE_PORT" ] || CONSOLE_PORT="$(grep -oE 'CONSOLE_PORT:-[0-9]+' docker-compose.release.yml | head -1 | cut -d- -f2)"
    CONSOLE_PORT="${CONSOLE_PORT:-5001}"
else
    START_PORT="${CONSOLE_PORT:-5001}"
    CONSOLE_PORT="$(find_free_port "$START_PORT")" || {
        echo "Couldn't find a free port between $START_PORT and $(( START_PORT + 20 ))."
        echo "Something unusual is going on with this machine's networking."
        echo ""
        pause
        exit 1
    }
    if [ "$CONSOLE_PORT" != "$START_PORT" ]; then
        echo "Port $START_PORT is already taken, so this will use $CONSOLE_PORT instead."
        echo ""
    fi
fi
# Add the Watchtower scope to a compose file written by an older
# installer.
#
# The file is only written when absent, so a machine installed before the
# scope existed would keep an unscoped Watchtower forever -- and an
# unscoped Watchtower deletes the other product's, silently, the moment
# the second product is installed. Re-running the installer is the
# documented repair path, so it has to actually repair this.
#
# Idempotent: does nothing once a scope is present.
ensure_watchtower_scope() {
    file="docker-compose.release.yml"
    [ -f "$file" ] || return 0
    grep -q "watchtower.scope=" "$file" && return 0

    cp "$file" "$file.bak"
    awk -v scope="catchupgist" '''
        { print }
        /com\.centurylinklabs\.watchtower\.enable=true/ {
            print "      - \"com.centurylinklabs.watchtower.scope=" scope "\""
        }
        /WATCHTOWER_LABEL_ENABLE=true/ {
            print "      - WATCHTOWER_SCOPE=" scope
        }
    ''' "$file.bak" > "$file"

    if grep -q "watchtower.scope=" "$file"; then
        echo "Updated docker-compose.release.yml so this product's updater"
        echo "leaves the other product's alone (previous file kept as"
        echo "docker-compose.release.yml.bak)."
        echo ""
    else
        # Nothing matched -- a hand-edited file. Leave it exactly as it
        # was rather than guessing.
        mv "$file.bak" "$file"
    fi
}

# Older Gist installs published the console on 127.0.0.1 only. Host
# networking lets it follow the network mode chosen on Settings (sibling
# PRD 9), and the port moves into the environment.
ensure_host_network() {
    file="docker-compose.release.yml"
    [ -f "$file" ] || return 0
    grep -q "CONSOLE_NETWORK=host" "$file" && return 0
    # Either a fixed port (what this installer wrote) or the repo's own
    # compose file's "${CONSOLE_PORT:-5001}" form.
    port="$(grep -oE '127\.0\.0\.1:[0-9]+:5000' "$file" | head -1 | cut -d: -f2)"
    [ -n "$port" ] || port="$(grep -oE '127\.0\.0\.1:\$\{CONSOLE_PORT:-[0-9]+\}:5000' "$file" \
        | head -1 | grep -oE ':-[0-9]+' | tr -d ':-')"
    [ -n "$port" ] || return 0

    cp "$file" "$file.bak"
    awk -v port="$port" '
        /^    ports:/ { skip = 1; print "    network_mode: host"; next }
        skip && /^      / { next }
        { skip = 0; print }
        /^      - DATA_DIR=\/app\/data/ {
            print "      - CONSOLE_PORT=" port
            print "      - CONSOLE_NETWORK=host"
        }
    ' "$file.bak" > "$file"

    if grep -q "CONSOLE_NETWORK=host" "$file" && grep -q "network_mode: host" "$file" \
            && ! grep -q "127\.0\.0\.1:.*:5000" "$file"; then
        echo "Updated docker-compose.release.yml so the Admin Console can be reached"
        echo "from your other devices once you choose that on Settings (it still"
        echo "answers on this computer only until then). Previous file kept as"
        echo "docker-compose.release.yml.bak."
        echo ""
    else
        mv "$file.bak" "$file"
    fi
}

# Older installs checked for updates 24h after the updater started, and a
# restart reset that clock; a fixed daily time doesn't drift.
ensure_update_schedule() {
    file="docker-compose.release.yml"
    [ -f "$file" ] || return 0
    grep -q "WATCHTOWER_POLL_INTERVAL=" "$file" || return 0
    cp "$file" "$file.bak"
    sed 's/WATCHTOWER_POLL_INTERVAL=[0-9]*/WATCHTOWER_SCHEDULE=0 0 4 * * */' "$file.bak" > "$file"
    if grep -q "WATCHTOWER_SCHEDULE=" "$file"; then
        echo "Updated docker-compose.release.yml to check for updates daily at 04:00 UTC."
        echo ""
    else
        mv "$file.bak" "$file"
    fi
}

mkdir -p secrets

if [ ! -f docker-compose.release.yml ]; then
    cat > docker-compose.release.yml <<EOF
# Declared, not inferred from the folder name -- see
# docs/co-installation.md.
name: catchupgist

services:
  app:
    image: ghcr.io/pppoole/email-summary-pipeline:\${APP_IMAGE_TAG:-latest}
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
      # Scoped so CatchUpCast and CatchUpGist can share a machine.
      # Watchtower removes other Watchtower instances by default, so
      # without a scope one product's updater would stop the other's.
      - "com.centurylinklabs.watchtower.scope=catchupgist"
    # Host networking, as CatchUpCast uses: the console answers on this
    # computer only until you choose otherwise on Settings, and then asks
    # for a sign-in.
    network_mode: host
    volumes:
      - db-data:/app/data
    environment:
      - DATA_DIR=/app/data
      - CONSOLE_PORT=${CONSOLE_PORT}
      - CONSOLE_NETWORK=host
    secrets:
      - bootstrap_key
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  watchtower:
    image: ghcr.io/nicholas-fedor/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_LABEL_ENABLE=true
      - WATCHTOWER_SCOPE=catchupgist
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_SCHEDULE=0 0 4 * * *
    restart: unless-stopped

secrets:
  bootstrap_key:
    file: ./secrets/bootstrap_key

volumes:
  db-data:
EOF
fi

ensure_watchtower_scope
ensure_host_network
ensure_update_schedule

if [ -f secrets/bootstrap_key ]; then
    echo "Bootstrap key already exists, skipping."
else
    echo "Generating the bootstrap encryption key..."
    # Created as the invoking user, not root. Without --user the file lands
    # as 600 root:root, and the very next thing this script does is tell the
    # person to back it up -- which fails with "Permission denied". Found by
    # running this installer on a clean machine. The container runs as root
    # and can read the file whichever way it is owned, so this costs nothing.
    docker run --rm --user "$(id -u):$(id -g)" \
        -v "$(pwd)/secrets:/app/secrets" \
        -e BOOTSTRAP_KEY_PATH=/app/secrets/bootstrap_key \
        ghcr.io/pppoole/email-summary-pipeline:latest \
        python -m email_summary_pipeline.cli init-secret >/dev/null
    echo ""
    echo ""
fi

# `up -d` alone never checks the registry for a newer image -- it just
# recreates the container from whatever's already cached locally, so
# re-running this file to pick up an update silently did nothing without
# this pull first.
echo "Checking for a newer image..."
docker compose -f docker-compose.release.yml pull --quiet
echo "Starting the containers..."
docker compose -f docker-compose.release.yml up -d --force-recreate

echo ""
echo "=============================="
echo "Done. Open this in your browser:"
echo ""
echo "    http://127.0.0.1:${CONSOLE_PORT}"
echo ""
echo "=============================="
echo ""
# Repeated here, last, with the real path: the first warning scrolled
# away during the download, and a key that is lost cannot be recovered
# (PRD 23, S-7). The console's Backup page is the easier way to keep it.
echo "Your settings are encrypted with a key kept in"
echo ""
echo "    $(pwd)/secrets/bootstrap_key"
echo ""
echo "You don't need to copy it. Once setup is done, use Backup in the Admin"
echo "Console instead: one file and a passphrase restore everything on any computer."
echo ""
if command -v xdg-open >/dev/null 2>&1; then
    sleep 2
    xdg-open "http://127.0.0.1:${CONSOLE_PORT}" >/dev/null 2>&1 || true
fi

pause
