#!/bin/bash

# The snapchain installation script. This script is used to install the latest version of snapchain.
# It can also be used to upgrade an existing installation of snapchain, also upgrading
# itself in the process.

# Define the version of this script
CURRENT_VERSION="1"

REPO="farcasterxyz/snapchain"
RAWFILE_BASE="https://raw.githubusercontent.com/$REPO"
LATEST_TAG="@latest"

DOCKER_COMPOSE_FILE_PATH="docker-compose.mainnet.yml"
SCRIPT_FILE_PATH="scripts/snapchain.sh"
GRAFANA_DASHBOARD_JSON_PATH="grafana/grafana-dashboard.json"
GRAFANA_INI_PATH="grafana/grafana.ini"

install_jq() {
    if command -v jq >/dev/null 2>&1; then
        echo "✅ Dependencies are installed."
        return 0
    fi

    echo "Installing jq..."

    # macOS
    if [[ "$(uname)" == "Darwin" ]]; then
        if command -v brew >/dev/null 2>&1; then
            brew install jq
        else
            echo "Homebrew is not installed. Please install Homebrew first."
            return 1
        fi

    # Ubuntu/Debian
    elif [[ -f /etc/lsb-release ]] || [[ -f /etc/debian_version ]]; then
        sudo apt-get update
        sudo apt-get install -y jq

    # RHEL/CentOS
    elif [[ -f /etc/redhat-release ]]; then
        sudo yum install -y jq

    # Fedora
    elif [[ -f /etc/fedora-release ]]; then
        sudo dnf install -y jq

    # openSUSE
    elif [[ -f /etc/os-release ]] && grep -q "ID=openSUSE" /etc/os-release; then
        sudo zypper install -y jq

    # Arch Linux
    elif [[ -f /etc/arch-release ]]; then
        sudo pacman -S jq

    else
        echo "Unsupported operating system. Please install jq manually."
        return 1
    fi

    echo "✅ jq installed successfully."
}

# Fetch file from repo at "@latest"
fetch_file_from_repo() {
    local file_path="$1"
    local local_filename="$2"

    local download_url
    download_url="$RAWFILE_BASE/$LATEST_TAG/$file_path?t=$(date +%s)"

    # Download the file using curl, and save it to the local filename. If the download fails,
    # exit with an error.
    curl -sS -o "$local_filename" "$download_url" || { echo "Failed to fetch $download_url."; exit 1; }
}

# Upgrade the script
self_upgrade() {
    # To allow easier testing
    if key_exists "SKIP_SELF_UPGRADE"; then
      echo "Skipping self upgrade"
      return 1
    fi

    local tmp_file
    trap 'rm -f "$tmp_file"' EXIT
    tmp_file=$(mktemp)
    fetch_file_from_repo "$SCRIPT_FILE_PATH" "$tmp_file"

    local current_hash
    local new_hash

    # Get the hash of the current script and the new file
    current_hash=$($HASH_CMD "$0" | awk '{ print $1 }')
    new_hash=$($HASH_CMD "$tmp_file" | awk '{ print $1 }')

    # Compare the hashes
    if [[ "$new_hash" != "$current_hash" ]]; then
        echo "New version found. Upgrading..."
        mv "$tmp_file" "$0" # Overwrite the current script
        chmod +rx "$0"      # Ensure the script remains executable
        echo "✅ Upgrade complete. Restarting with new version..."
        echo ""
        exec "$0" "$@" || echo "Exec failed with status: $?"

        # Exit the script because we already "exec"ed the script above
        exit 0
    else
        echo "✅ Latest Script Version."
        rm -f "$tmp_file"  # Clean up temporary file if no upgrade was needed
    fi
}

# Fetch the docker-compose.yml and grafana-dashboard.json files
fetch_latest_docker_compose_and_dashboard() {
    fetch_file_from_repo "$DOCKER_COMPOSE_FILE_PATH" "docker-compose.yml"
    fetch_file_from_repo "$GRAFANA_DASHBOARD_JSON_PATH" "grafana-dashboard.json"
    mkdir -p grafana
    chmod 777 grafana
    fetch_file_from_repo "$GRAFANA_INI_PATH" "grafana/grafana.ini"
}

# Prompt for hub operator agreement
prompt_for_hub_operator_agreement() {
    env_file=".env"

    update_env_file() {
        key="AGREE_NO_REWARDS_FOR_ME"
        value="true"
        temp_file="${env_file}.tmp"

        if [ -f "$env_file" ]; then
            # File exists, update or append
            updated=0
            while IFS= read -r line || [ -n "$line" ]; do
                if [ "${line%%=*}" = "$key" ]; then
                    echo "$key=$value" >>"$temp_file"
                    updated=1
                else
                    echo "$line" >>"$temp_file"
                fi
            done <"$env_file"

            if [ $updated -eq 0 ]; then
                echo "$key=$value" >>"$temp_file"
            fi

            mv "$temp_file" "$env_file"
        else
            # File doesn't exist, create it
            echo "$key=$value" >"$env_file"
        fi
    }

    prompt_agreement() {
        tried=0
        while true; do
            printf "⚠️  IMPORTANT: You will NOT get any rewards for running this node\n"
            printf "> Please type \"Yes\" to continue: "
            read -r response
            case $(printf "%s" "$response" | tr '[:upper:]' '[:lower:]') in
            yes | y)
                printf "✅ You have agreed to the terms of service. Proceeding...\n"
                update_env_file
                return 0
                ;;
            *)
                tried=$((tried + 1))
                if [ $tried -gt 10 ]; then
                    printf "❌ You have not agreed to the terms of service. Please run script again manually to agree and continue.\n"
                    return 1
                fi
                printf "[i] Incorrect input. Please try again.\n"
                ;;
            esac
        done
    }

    if grep -q "AGREE_NO_REWARDS_FOR_ME=true" "$env_file"; then
        printf "✅ You have agreed to the terms of service. Proceeding...\n"
        return 0
    else
        # Check if stdin is a terminal
        if [ -t 0 ]; then
            prompt_agreement
            return $?
        fi

        # If we've reached this point, shut down existing services since agreement is required

        # Setup the docker-compose command
        set_compose_command

        # Run docker compose down
        $COMPOSE_CMD down
        printf "❌ You have not agreed to the terms of service. Please run script again manually to agree and continue.\n"

        return 1
    fi
}

store_operator_fid_env() {
    local input
    local response

    read -p "> Your FID or farcaster username: " input
    if [[ -z $input ]]; then
        response=""
    elif [[ $input =~ ^-?[0-9]+$ ]]; then
        response=$(curl -s "https://fnames.farcaster.xyz/transfers?fid=$input" | jq '.transfers[-1].to')
    else
        response=$(curl -s "https://fnames.farcaster.xyz/transfers?name=$input" | jq '.transfers[-1].to')
    fi

    if [ "$response" != "null" ] && [ "$response" != "" ]; then
        echo "HUB_OPERATOR_FID=$response" >> .env
    else
        echo "Not a valid FID or username. Not updating HUB_OPERATOR_FID."
        echo "HUB_OPERATOR_FID=0" >> .env
    fi
}

key_exists() {
    local key=$1
    grep -q "^$key=" .env
    return $?
}

write_env_file() {
    if [[ ! -f .env ]]; then
        touch .env
    fi

    if ! key_exists "FC_NETWORK_ID"; then
        echo "FC_NETWORK_ID=1" >> .env
    fi

    if ! key_exists "STATSD_METRICS_SERVER"; then
        echo "STATSD_METRICS_SERVER=statsd:8125" >> .env
    fi

    if ! key_exists "HUB_OPERATOR_FID"; then
        store_operator_fid_env
    fi

    echo "✅ .env file updated."
}

ensure_grafana() {
      # Create a grafana data directory if it doesn't exist
      mkdir -p grafana/data
      chmod 777 grafana/data

      if $COMPOSE_CMD ps 2>&1 >/dev/null; then
          if $COMPOSE_CMD ps statsd | grep -q "Up"; then
              $COMPOSE_CMD restart statsd grafana
          else
              $COMPOSE_CMD up -d statsd grafana
          fi
      else
          echo "❌ Docker is not running or there's another issue with Docker. Please start Docker manually."
          exit 1
      fi
}

## Configure Grafana
setup_grafana() {
    local grafana_url="http://127.0.0.1:3000"
    local credentials
    local response dashboard_uid prefs

    if key_exists "GRAFANA_CREDS"; then
        credentials=$(grep "^GRAFANA_CREDS=" .env | awk -F '=' '{printf $2}')
        echo "Using grafana creds from .env file"
    else
        credentials="admin:admin"
    fi

    add_datasource() {
        response=$(curl -s -o /dev/null -w "%{http_code}" -X "POST" "$grafana_url/api/datasources" \
                -u "$credentials" \
                -H "Content-Type: application/json" \
                --data-binary '{
            "name":"Graphite",
            "type":"graphite",
            "url":"http://statsd:80",
            "access":"proxy"
        }')

        # Handle if the datasource already exists
        if [[ "$response" == "409" ]]; then
             echo "✅ Datasource 'Graphite' exists."
            response="200"
        fi
    }

    # Step 1: Restart statsd and grafana if they are running, otherwise start them
    ensure_grafana

    # Step 2: Wait for Grafana to be ready
    echo "Waiting for Grafana to be ready..."
    while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' $grafana_url/api/health)" != "200" ]]; do
        sleep 2;
    done
    echo "Grafana is ready."

    # Step 3: Add Graphite as a data source using Grafana's API
    add_datasource

    # Check if the default credentials failed
    if [[ "$response" == "401" ]]; then
        echo "Please enter your Grafana credentials."
        read -p "Username: " username
        read -sp "Password: " password
        echo
        credentials="$username:$password"

        # Retry adding the data source with the new credentials
        add_datasource

        if [[ "$response" != "200" ]]; then
            echo "Failed to add data source with provided credentials. Exiting."
            return 1
        fi
    fi

    # Step 4: Import the dashboard. The API takes a slightly different format than the JSON import
    # in the UI, so we need to convert the JSON file first.
    jq '{dashboard: (del(.id) | . + {id: null}), folderId: 0, overwrite: true}' "grafana-dashboard.json" > "grafana-dashboard.api.json"

    response=$(curl -s -X "POST" "$grafana_url/api/dashboards/db" \
        -u "$credentials" \
        -H "Content-Type: application/json" \
        --data-binary @grafana-dashboard.api.json)

    rm "grafana-dashboard.api.json"

    if echo "$response" | jq -e '.status == "success"' >/dev/null; then
        # Extract dashboard UID from the response
        dashboard_uid=$(echo "$response" | jq -r '.uid')

        # Set the default home dashboard for the organization
        prefs=$(curl -s -X "PUT" "$grafana_url/api/org/preferences" \
            -u "$credentials" \
            -H "Content-Type: application/json" \
            --data "{\"homeDashboardUID\":\"$dashboard_uid\"}")

        echo "✅ Dashboard is installed."
    else
        echo "Failed to install the dashboard. Exiting."
        echo "$response"
        return 1
    fi
}

install_docker() {
    # Check if Docker is already installed
    if command -v docker &> /dev/null; then
        echo "✅ Docker is installed."
        return 0
    fi

    # Install using Docker's convenience script
    curl -fsSL https://get.docker.com -o get-docker.sh
    sh get-docker.sh
    if [[ $? -ne 0 ]]; then
        echo "❌ Failed to install Docker via official script. Falling back to docker-compose."
        curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
        chmod +x /usr/local/bin/docker-compose
    fi
    rm get-docker.sh

    # Add current user to the docker group
    sudo usermod -aG docker $(whoami)

    echo "✅ Docker is installed"
    return 0
}

setup_crontab() {
    # Check if crontab is installed
    if ! command -v crontab &> /dev/null; then
        echo "❌ crontab is not installed. Please install crontab first."
        exit 1
    fi

    # skip installing crontab if SKIP_CRONTAB is set to anything in the .env
    if key_exists "SKIP_CRONTAB"; then
        echo "✅ SKIP_CRONTAB exists in .env. Skipping crontab setup."
        return 0
    fi

    # If the crontab was installed for the current user (instead of root) then
    # remove it
    if [[ "$(uname)" == "Linux" ]]; then
        # Extract the username from the current directory, since we're running as root
        local user=$(pwd | cut -d/ -f3)
        USER_CRONTAB_CMD="crontab -u ${user}"

        if $USER_CRONTAB_CMD -l 2>/dev/null | grep -q "snapchain.sh"; then
            $USER_CRONTAB_CMD -l > /tmp/temp_cron.txt
            sed -i '/snapchain\.sh/d' /tmp/temp_cron.txt
            $USER_CRONTAB_CMD /tmp/temp_cron.txt
            rm /tmp/temp_cron.txt
        fi
    fi

    # Check if the crontab file is already installed
    if $CRONTAB_CMD -l 2>/dev/null | grep -q "snapchain.sh"; then
      # Fix buggy crontab entry which would run every minute
      if $CRONTAB_CMD -l 2>/dev/null | grep "snapchain.sh" | grep -q "^\*"; then
        echo "Removing crontab for upgrade"

        # Export the existing crontab entries to a temporary file in /tmp/
        crontab -l > /tmp/temp_cron.txt

        # Remove the line containing "snapchain.sh" from the temporary file
        sed -i '/snapchain\.sh/d' /tmp/temp_cron.txt
        crontab /tmp/temp_cron.txt
        rm /tmp/temp_cron.txt
      else
        echo "✅ crontab entry is already installed."
        return 0
      fi
    fi

    local content_to_hash
    local hub_operator_fid
    hub_operator_fid=$(grep "^HUB_OPERATOR_FID=" .env | cut -d= -f2)
    # If the HUB_OPERATOR_FID is set and it is not 0, then use it to determine the day of week
    if [[ -n "$hub_operator_fid" ]] && [[ "$hub_operator_fid" != "0" ]]; then
        content_to_hash=$(echo -n "$hub_operator_fid")
        echo "auto-upgrade: Using HUB FID to determine upgrade day $content_to_hash"
    elif [ -f "./.hub/default_id.protobuf" ]; then
        content_to_hash=$(cat ./.hub/default_id.protobuf)
        echo "auto-upgrade: Using Peer Identity to determine upgrade day"
    else
        echo "auto-upgrade: Unable to determine upgrade day"
        exit 1
    fi

    # Pick a random weekday based on the sha of the operator FID or peer identity
    local sha=$(echo -n "${content_to_hash}" | $HASH_CMD | awk '{ print $1 }')
    local day_of_week=$(( ( 0x${sha:0:8} % 5 ) + 1 ))
    # Pick a random hour between midnight and 6am
    local hour=$((RANDOM % 7))
    local crontab_entry="0 $hour * * $day_of_week $(pwd)/snapchain.sh autoupgrade >> $(pwd)/snapchain-autoupgrade.log 2>&1"
    if ($CRONTAB_CMD -l 2>/dev/null; echo "${crontab_entry}") | $CRONTAB_CMD -; then
        echo "✅ added auto-upgrade to crontab (0 $hour * * $day_of_week)"
    else
        echo "❌ failed to add auto-upgrade to crontab"
    fi
}

start_snapchain() {

    # Stop the "snapchain" service if it is already running
    $COMPOSE_CMD stop snapchain

    # Start the "snapchain" service
    $COMPOSE_CMD up -d snapchain
}

cleanup() {
  # Prune unused docker cruft. Make sure to call this only when hub is already running
  echo "Pruning unused docker images and volumes"
  docker system prune --volumes -f
}

set_compose_command() {
    # Detect whether "docker-compose" or "docker compose" is available
    if command -v docker-compose &> /dev/null; then
        COMPOSE_CMD="docker-compose"
        echo "✅ Using docker-compose"
    elif docker compose version &> /dev/null; then
        COMPOSE_CMD="docker compose"
        echo "✅ Using docker compose"
    else
        echo "❌ Neither 'docker-compose' nor 'docker compose' is available on this system."
        exit 1
    fi
}

set_platform_commands() {
    # Determine the appropriate hash command to use
    if command -v sha256sum > /dev/null; then
        HASH_CMD="sha256sum"
    elif command -v shasum > /dev/null; then
        HASH_CMD="shasum -a 256"
    else
        echo "Error: No suitable hash command found."
        exit 1
    fi

    CRONTAB_CMD="crontab"
}

reexec_as_root_if_needed() {
    # Check if on Linux
    if [[ "$(uname)" == "Linux" ]]; then
        # Check if not running as root, then re-exec as root
        if [[ "$(id -u)" -ne 0 ]]; then
            # Ensure the script runs in the ~/snapchain directory
            cd ~/snapchain || { echo "Failed to switch to ~/snapchain directory."; exit 1; }
            exec sudo "$0" "$@"
        else
            # If the current directory is not named "snapchain", change to "~/snapchain"
            if [[ "$(basename "$PWD")" != "snapchain" ]]; then
                cd "$(dirname "$0")" || { echo "Failed to switch to ~/snapchain directory."; exit 1; }
            fi
            echo "✅ Running on Linux ($(pwd))."
        fi
    # Check if on macOS
    elif [[ "$(uname)" == "Darwin" ]]; then
        cd ~/snapchain || { echo "Failed to switch to ~/snapchain directory."; exit 1; }
        echo "✅ Running on macOS $(pwd)."
    fi
}


# Call the function at the beginning of your script
reexec_as_root_if_needed "$@"

# Prompt for hub operator agreement
prompt_for_hub_operator_agreement || exit $?

# Check for the "up" command-line argument
if [ "$1" == "up" ]; then
   # Setup the docker-compose command
    set_compose_command

    # Run docker compose up -d snapchain
    $COMPOSE_CMD up -d snapchain statsd # grafana

    echo "✅ snapchain is running."

    # Finally, start showing the logs
    $COMPOSE_CMD logs --tail 100 -f snapchain

    exit 0
fi

# "down" command-line argument
if [ "$1" == "down" ]; then
    # Setup the docker-compose command
    set_compose_command

    # Run docker compose down
    $COMPOSE_CMD down

    echo "✅ snapchain is stopped."

    exit 0
fi

# Check the command-line argument for 'upgrade'
if [ "$1" == "upgrade" ]; then
    # Ensure the ~/snapchain directory exists
    if [ ! -d ~/snapchain ]; then
        mkdir -p ~/snapchain || { echo "Failed to create ~/snapchain directory."; exit 1; }
    fi

    # Install dependencies
    install_jq

    set_platform_commands

    # Upgrade this script itself
    self_upgrade "$@"

    # Call the function to install docker
    install_docker "$@"

    # Call the function to set the COMPOSE_CMD variable
    set_compose_command

    # Update the env file if needed
    write_env_file

    # Fetch the latest docker-compose.yml
    fetch_latest_docker_compose_and_dashboard

    # Setup the Grafana dashboard
    setup_grafana

    setup_crontab

    # Start the snapchain service
    start_snapchain

    echo "✅ Upgrade complete."
    echo ""
    echo "Monitor your node at http://localhost:3000/"

    # Sleep for 5 seconds
    sleep 5

    # Finally, start showing the logs
    $COMPOSE_CMD logs --tail 100 -f snapchain

    exit 0
fi

# Show logs of the snapchain service
if [ "$1" == "logs" ]; then
    set_compose_command
    $COMPOSE_CMD logs --tail 100 -f snapchain
    exit 0
fi

if [ "$1" == "autoupgrade" ]; then
    # Autoupgrade cronjob needs the correct $PATH entries
    if [[ ! -f ~/.bashrc ]]; then
      source ~/.bashrc
    fi

    echo "$(date) Attempting snapchain autoupgrade..."

    # Since cronjob is running under root, make sure the dependencies are installed
    install_jq
    install_docker "$@"

    set_platform_commands
    set_compose_command

    # Upgrade this script itself, fetch the latest docker-compose.yml, and restart the containers
    self_upgrade "$@"
    fetch_latest_docker_compose_and_dashboard
    ensure_grafana
    start_snapchain
    sleep 5
    cleanup

    echo "$(date) Completed snapchain autoupgrade"

    exit 0
fi

# If run without args OR with "help", show a help
if [ $# -eq 0 ] || [ "$1" == "help" ]; then
    echo "snapchain.sh - Install or upgrade snapchain"
    echo "Usage:     snapchain.sh [command]"
    echo "  upgrade  Upgrade an existing installation of snapchain"
    echo "  logs     Show the logs of the snapchain service"
    echo "  up       Start snapchain and Grafana dashboard"
    echo "  down     Stop snapchain and Grafana dashboard"
    echo "  help     Show this help"
    echo ""
    echo "add SKIP_CRONTAB=true to your .env to skip installing the autoupgrade crontab"
    exit 0
fi

echo "❌ Invalid command: $1"
echo ""
exec "$0" help
