#!/bin/bash
# Run this from a terminal:
#   chmod +x install-catchupcast-linux.sh && ./install-catchupcast-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="catchupcast"
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-podcast-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 "CatchUpCast - 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 Admin Console port. 5000 by default, but taken if AirPlay Receiver
# is on (macOS reserves 5000 and 7000 for it, and it is on by default),
# so tick to the next free one rather than failing with advice about
# environment variables that a double-click user cannot act on.
if [ -f docker-compose.release.yml ]; then
    CONSOLE_PORT="$(grep -oE 'CONFIGURATOR_PORT=[0-9]+' docker-compose.release.yml | head -1 | cut -d= -f2)"
    CONSOLE_PORT="${CONSOLE_PORT:-5000}"
else
    START_PORT="${CONSOLE_PORT:-5000}"
    CONSOLE_PORT="$(find_free_port "$START_PORT")" || {
        echo "Couldn't find a free port between $START_PORT and $(( START_PORT + 20 ))."
        echo ""
        pause
        exit 1
    }
    if [ "$CONSOLE_PORT" != "$START_PORT" ]; then
        echo "Port $START_PORT is already taken (on a Mac that is usually"
        echo "AirPlay Receiver), 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="catchupcast" '''
        { 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 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: email-podcast-pipeline

services:
  app:
    image: ghcr.io/pppoole/email-podcast-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=catchupcast"
    network_mode: host
    volumes:
      - db-data:/app/data
    environment:
      - DB_PATH=/app/data/podcast.db
      - MESSAGES_DB_PATH=/app/data/messages.db
      - CONFIGURATOR_PORT=${CONSOLE_PORT}
      - TLS_CERT_DIR=/app/data/tls
    secrets:
      - bootstrap_key
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "python", "-m", "email_podcast_pipeline.healthcheck"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    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=catchupcast
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_SCHEDULE=0 0 4 * * *
    restart: unless-stopped

secrets:
  bootstrap_key:
    file: ./secrets/bootstrap_key

volumes:
  db-data:
EOF
    echo "Wrote docker-compose.release.yml"
    echo ""
fi

ensure_watchtower_scope
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" ghcr.io/pppoole/email-podcast-pipeline:latest \
        python -m email_podcast_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
