#!/bin/bash

# -----------------------------------------------------------------------------
# ReplikSQL v2.3 - Bidirectional MySQL (file/pos) replication bootstrapper
# Author   : Christophe Casalegno / Brain 0verride - refactor 23-Apr-2025
# Licence  : GPL-3.0 or later
#
# Copyright (c) 2025 Christophe Casalegno
#
# This program is free software: you can redistribute it and/or modify
#
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>
#
# The license is available on this server here:
# https://www.christophe-casalegno.com/licences/gpl-3.0.txt

# -----------------------------------------------------------------------------
# Example  : repliksql \
#              --master-pub 83.143.18.20 --master-priv 192.168.1.20 --master-pw XXX \
#              --slave-pub  83.143.18.30 --slave-priv  192.168.1.30 --slave-pw  YYY \
#              --master-ssh-port 2222 --slave-ssh-port 2223
# -----------------------------------------------------------------------------

set -Eeuo pipefail
IFS=$'\n\t'

# Init vars for set -u safety
MASTER_PUB=""; SLAVE_PUB=""; MASTER_PRIV=""; SLAVE_PRIV=""
MASTER_PW=""; SLAVE_PW=""
MASTER_SSH_PORT=65022
SLAVE_SSH_PORT=65022
LOCAL_TMP_DIR=""
REMOTE_TMP_DIR="/tmp/repliksql"
REPL_PW=""

# -----------------------------------------------------------------------------
# Colour setup
# -----------------------------------------------------------------------------

if [ "${NO_COLOR:-0}" = 1 ] 
then
    RED=""; GREEN=""; YELLOW=""; NC=""
else
    RED="\033[0;31m"; GREEN="\033[0;32m"; YELLOW="\033[1;33m"; NC="\033[0m"
fi

# -----------------------------------------------------------------------------
# Logging & helpers
# -----------------------------------------------------------------------------

function log()   { printf '%s %b%s%b\n' "$(date +%FT%T)" "${GREEN}" "$*" "${NC}" >&2; }
function warn()  { printf '%s %b%s%b\n' "$(date +%FT%T)" "${YELLOW}" "$*" "${NC}" >&2; }
function fatal() { printf '%s %b%s%b\n' "$(date +%FT%T)" "${RED}" "$*" "${NC}" >&2; exit 1; }

function usage() {
cat <<EOF
RepliSQL v2 – bootstrap bidirectional MySQL replication

Required:
  --master-pub|-M     Master's public IP
  --master-pw|-P      Master MySQL root password
  --slave-pub|-S      Slave's public IP
  --slave-pw|-p       Slave MySQL root password

Optional:
  --master-priv|-m    Master's private IP (default = master-pub)
  --slave-priv|-s     Slave's private IP  (default = slave-pub)
  --master-ssh-port   Master SSH port (default 65022)
  --slave-ssh-port    Slave SSH port  (default 65022)
  --help|-h           Show this help
EOF
exit 1
}

function check_dep() { for bin in "$@"; do command -v "$bin" >/dev/null || fatal "$bin is missing in \$PATH"; done; }

function check_ip() { [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || fatal "$1 is not a valid IP"; }

function gen_pass() { pwgen -A -B 12 1; }

function port_for_host() { [ "$1" = "$MASTER_PUB" ] && echo "$MASTER_SSH_PORT" || echo "$SLAVE_SSH_PORT"; }

function remote_exec() 
{
    local host="$1" cmd="$2" port
    port="$(port_for_host "$host")"
    ssh -o BatchMode=yes -p "$port" root@"$host" "$cmd"
}

function remote_copy() 
{
    local host="$1" src="$2" dst="$3" port
    port="$(port_for_host "$host")"
    scp -p -q -P "$port" "$src" root@"$host":"$dst"
}

function restart_sql() 
{
    local host="$1"
    remote_exec "$host" "systemctl restart mysql 2>/dev/null || service mysql restart"
}

function run_sql_file() { remote_exec "$1" "MYSQL_PWD='$2' mysql --silent < '$3'"; }
function run_sql_inline() { remote_exec "$1" "MYSQL_PWD='$2' mysql --silent -e \"$3\""; }

function get_server_id() { remote_exec "$1" "mysql -p$2 -Nse 'SELECT @@server_id;'"; }

function get_master_status() { remote_exec "$1" "mysql -p$2 -e 'SHOW MASTER STATUS\\G'" | awk '/File:/ {file=$2} /Position:/ {pos=$2} END {print file":"pos}'; }

function check_replication() { remote_exec "$1" "mysql -p$2 -e 'SHOW SLAVE STATUS\\G'" | awk '/Slave_IO_Running:/ {io=$2} /Slave_SQL_Running:/ {sql=$2} END {print io,sql}'; }

function create_replication_user() 
{
    log "Creating replication user"

    cat >"$LOCAL_TMP_DIR/repl_user.sql" <<SQL
CREATE USER IF NOT EXISTS 'replikuser'@'%' IDENTIFIED BY '$REPL_PW';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'replikuser'@'%' IDENTIFIED BY '$REPL_PW' REQUIRE SSL;
FLUSH PRIVILEGES;
SQL
}

function slave_setup_file() 
{
    cat >"$LOCAL_TMP_DIR/slave_setup.sql" <<SQL
STOP SLAVE;
CHANGE MASTER TO MASTER_HOST = '$MASTER_PRIV', MASTER_USER = 'replikuser', MASTER_PASSWORD = '$REPL_PW',
MASTER_LOG_FILE = '$MASTER_FILE', MASTER_LOG_POS = $MASTER_POS;
START SLAVE;
SQL
}

function master_setup_file() 
{
    cat >"$LOCAL_TMP_DIR/master_setup.sql" <<SQL
STOP SLAVE;
CHANGE MASTER TO MASTER_HOST = '$SLAVE_PRIV', MASTER_USER = 'replikuser', MASTER_PASSWORD = '$REPL_PW',
MASTER_LOG_FILE = '$SLAVE_FILE', MASTER_LOG_POS = $SLAVE_POS;
START SLAVE;
SQL
}

function cleanup() 
{
    [[ -n "$LOCAL_TMP_DIR" && -d "$LOCAL_TMP_DIR" ]] && rm -rf "$LOCAL_TMP_DIR"
    if [[ -n "$MASTER_PUB" && -n "$SLAVE_PUB" ]]; then
        for host in "$MASTER_PUB" "$SLAVE_PUB"; do
            remote_exec "$host" "rm -rf '$REMOTE_TMP_DIR'" || true
        done
    fi
}

# -----------------------------------------------------------------------------
# Parse args
# -----------------------------------------------------------------------------

while [[ $# -gt 0 ]] 
do
    case "$1" in
        --master-pub|-M) MASTER_PUB="$2"; shift 2 ;;
        --master-priv|-m) MASTER_PRIV="$2"; shift 2 ;;
        --master-pw|-P) MASTER_PW="$2"; shift 2 ;;
        --slave-pub|-S) SLAVE_PUB="$2"; shift 2 ;;
        --slave-priv|-s) SLAVE_PRIV="$2"; shift 2 ;;
        --slave-pw|-p) SLAVE_PW="$2"; shift 2 ;;
        --master-ssh-port) MASTER_SSH_PORT="$2"; shift 2 ;;
        --slave-ssh-port) SLAVE_SSH_PORT="$2"; shift 2 ;;
        --help|-h) usage ;;
        *) fatal "Unknown option: $1" ;;
    esac
done

# -----------------------------------------------------------------------------
# Main execution
# -----------------------------------------------------------------------------

trap cleanup EXIT INT TERM

: "${MASTER_PRIV:=$MASTER_PUB}"
: "${SLAVE_PRIV:=$SLAVE_PUB}"

for var in MASTER_PUB MASTER_PRIV MASTER_PW SLAVE_PUB SLAVE_PRIV SLAVE_PW 
do
    [ -z "${!var:-}" ] && fatal "$var is missing. See --help"
done

for ip in "$MASTER_PUB" "$MASTER_PRIV" "$SLAVE_PUB" "$SLAVE_PRIV" 
do
    check_ip "$ip"
done

check_dep pwgen ssh scp sed awk

log "Checking server-id values on both hosts..."

MASTER_ID="$(get_server_id "$MASTER_PUB" "$MASTER_PW")"
SLAVE_ID="$(get_server_id "$SLAVE_PUB" "$SLAVE_PW")"

if [ "$MASTER_ID" = "$SLAVE_ID" ] 
then
    warn "⚠ Both MySQL instances have the same server-id: $MASTER_ID"
    fatal "Please adjust 'server-id' in your MySQL config."
fi

log "Verified: distinct server-ids (master=$MASTER_ID, slave=$SLAVE_ID)"

LOCAL_TMP_DIR="$(mktemp -d)"
REPL_PW="$(gen_pass)"

for host in "$MASTER_PUB" "$SLAVE_PUB" 
do
    remote_exec "$host" "mkdir -p '$REMOTE_TMP_DIR'"
done

create_replication_user

for host in "$MASTER_PUB" "$SLAVE_PUB" 
do
    RPW=$([ "$host" = "$MASTER_PUB" ] && echo "$MASTER_PW" || echo "$SLAVE_PW")
    remote_copy "$host" "$LOCAL_TMP_DIR/repl_user.sql" "$REMOTE_TMP_DIR/"
    run_sql_file "$host" "$RPW" "$REMOTE_TMP_DIR/repl_user.sql"
    restart_sql "$host"
done

log "Fetching master status from master..."
MASTER_STATUS="$(get_master_status "$MASTER_PUB" "$MASTER_PW")"
MASTER_FILE="${MASTER_STATUS%%:*}"
MASTER_POS="${MASTER_STATUS##*:}"

slave_setup_file
remote_copy "$SLAVE_PUB" "$LOCAL_TMP_DIR/slave_setup.sql" "$REMOTE_TMP_DIR/"
run_sql_file "$SLAVE_PUB" "$SLAVE_PW" "$REMOTE_TMP_DIR/slave_setup.sql"

log "Fetching master status from slave (to reverse)..."
SLAVE_STATUS="$(get_master_status "$SLAVE_PUB" "$SLAVE_PW")"
SLAVE_FILE="${SLAVE_STATUS%%:*}"
SLAVE_POS="${SLAVE_STATUS##*:}"

master_setup_file
remote_copy "$MASTER_PUB" "$LOCAL_TMP_DIR/master_setup.sql" "$REMOTE_TMP_DIR/"
run_sql_file "$MASTER_PUB" "$MASTER_PW" "$REMOTE_TMP_DIR/master_setup.sql"

log "Waiting 5s for replication to start..."
sleep 5

errors=()

for host in "$MASTER_PUB" "$SLAVE_PUB" 
do
    RPW=$([ "$host" = "$MASTER_PUB" ] && echo "$MASTER_PW" || echo "$SLAVE_PW")
    result="$(check_replication "$host" "$RPW")"
    IO="${result%% *}"; SQL="${result##* }"
    log "$host → Slave_IO_Running=$IO Slave_SQL_Running=$SQL"
    [[ "$IO" != "Yes" || "$SQL" != "Yes" ]] && errors+=("$host: IO=$IO, SQL=$SQL")
done

if (( ${#errors[@]} > 0 )) 
then
    fatal "Replication error(s): ${errors[*]}"
else
    log "✅ Bi-directional replication up and running"
fi

