#!/bin/bash

# Redeploy script with robust handling for "Text file busy", file locks, and process lifecycle.
# Uses atomic binary swap to avoid overwriting running executables.

set -e

# Configuration
MAX_PORT_FREE_ATTEMPTS=20
MAX_PROCESS_EXIT_ATTEMPTS=30
MAX_FILE_RELEASE_ATTEMPTS=15
MAX_HEALTH_ATTEMPTS=60
FILE_OPERATION_RETRIES=5
GRACE_PERIOD_AFTER_KILL=2

# Function to display usage
usage() {
    echo "Usage: $0 --app-name <APP_NAME> --port <PORT> --app-dir <APP_DIR> [--crate-name <CRATE_NAME>]"
    exit 1
}

# Load defaults from xbp.json if not provided
load_defaults() {
    if [[ -z "$APP_NAME" || -z "$PORT" || -z "$APP_DIR" ]]; then
        SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
        XBP_JSON=""
        for candidate in "${SCRIPT_DIR}/xbp.json" "./xbp.json" ".xbp/xbp.json"; do
            if [[ -f "$candidate" ]]; then
                XBP_JSON="$candidate"
                break
            fi
        done
        if [[ -n "$XBP_JSON" ]]; then
            APP_NAME="${APP_NAME:-$(jq -r '.project_name' "$XBP_JSON")}"
            PORT="${PORT:-$(jq -r '.port' "$XBP_JSON")}"
            APP_DIR="${APP_DIR:-$(jq -r '.build_dir' "$XBP_JSON")}"
            CRATE_NAME="${CRATE_NAME:-$(jq -r '.crate_name' "$XBP_JSON")}"
        fi
    fi
}

# Get PIDs using a specific file (executable)
get_pids_using_file() {
    local path=$1
    if command -v lsof &> /dev/null; then
        lsof -t "$path" 2>/dev/null || true
    elif command -v fuser &> /dev/null; then
        fuser "$path" 2>/dev/null | tr -d ' ' || true
    else
        return 1
    fi
}

# Check if any process has a file open
is_file_in_use() {
    local path=$1
    [[ -n "$(get_pids_using_file "$path")" ]]
}

# Check if port is in use
is_port_in_use() {
    local port=$1
    if command -v ss &> /dev/null; then
        ss -tuln | grep -q ":${port} "
    elif command -v netstat &> /dev/null; then
        netstat -tuln | grep -q ":${port} "
    else
        lsof -i ":${port}" &> /dev/null
    fi
}

# Kill all processes on a port aggressively
kill_port() {
    local port=$1
    echo "  Killing processes on port $port..."

    if command -v fuser &> /dev/null; then
        fuser -k "${port}/tcp" 2>/dev/null || true
    fi

    if command -v lsof &> /dev/null; then
        local pids
        pids=$(lsof -t -i ":${port}" 2>/dev/null || true)
        if [[ -n "$pids" ]]; then
            echo "$pids" | xargs -r kill -9 2>/dev/null || true
        fi
    fi

    if command -v ss &> /dev/null; then
        local pids
        pids=$(ss -tlnp "sport = :${port}" 2>/dev/null | grep -oP 'pid=\K[0-9]+' || true)
        if [[ -n "$pids" ]]; then
            echo "$pids" | xargs -r kill -9 2>/dev/null || true
        fi
    fi

    xbp ports -p "$port" --kill 2>/dev/null || true
    sleep "$GRACE_PERIOD_AFTER_KILL"
}

# Wait for port to be free with retries and exponential backoff
wait_for_port_free() {
    local port=$1
    local attempt=1
    local wait_time=1

    echo "Waiting for port $port to be free (max $MAX_PORT_FREE_ATTEMPTS attempts)..."

    while is_port_in_use "$port"; do
        if [[ $attempt -ge $MAX_PORT_FREE_ATTEMPTS ]]; then
            echo "ERROR: Port $port still in use after $MAX_PORT_FREE_ATTEMPTS attempts"
            lsof -i ":${port}" 2>/dev/null || ss -tlnp "sport = :${port}" 2>/dev/null || true
            return 1
        fi

        echo "  Attempt $attempt/$MAX_PORT_FREE_ATTEMPTS: Port still in use, killing..."
        kill_port "$port"
        sleep "$wait_time"
        wait_time=$((wait_time < 5 ? wait_time + 1 : 5))
        attempt=$((attempt + 1))
    done

    echo "  Port $port is free"
    return 0
}

# Wait for a file to be released by all processes (no more open handles)
wait_for_file_released() {
    local path=$1
    local attempt=1

    [[ ! -f "$path" ]] && return 0

    echo "  Waiting for file to be released: $path (max $MAX_FILE_RELEASE_ATTEMPTS attempts)..."

    while is_file_in_use "$path"; do
        if [[ $attempt -ge $MAX_FILE_RELEASE_ATTEMPTS ]]; then
            echo "  WARNING: File still in use after $MAX_FILE_RELEASE_ATTEMPTS attempts"
            lsof "$path" 2>/dev/null || true
            return 1
        fi
        echo "    Attempt $attempt: File busy, waiting..."
        sleep 2
        attempt=$((attempt + 1))
    done

    echo "  File released"
    return 0
}

# Run command with retries (for transient "Text file busy", "Resource temporarily unavailable", etc.)
retry_op() {
    local max=$1
    local delay=$2
    shift 2
    local attempt=1

    while true; do
        if "$@"; then
            return 0
        fi
        local err=$?
        if [[ $attempt -ge $max ]]; then
            return $err
        fi
        echo "  Retry $attempt/$max failed (exit $err), waiting ${delay}s..."
        sleep "$delay"
        attempt=$((attempt + 1))
        delay=$((delay < 5 ? delay + 1 : 5))
    done
}

# Atomic deploy: copy to .new, stop process, wait for release, mv into place
atomic_binary_deploy() {
    local src=$1
    local dest=$2

    local dest_new="${dest}.new"
    local dest_old="${dest}.old"

    echo "Deploying binary atomically (avoids 'Text file busy')..."

    # 1. Copy to temp file first - never overwrites running binary
    if ! cp "$src" "$dest_new"; then
        echo "FATAL: Failed to copy $src to $dest_new"
        return 1
    fi
    chmod +x "$dest_new"

    # 2. Stop PM2 so process releases the old binary
    echo "  Stopping PM2 process $APP_NAME..."
    pm2 stop "$APP_NAME" 2>/dev/null || true
    pm2 delete "$APP_NAME" 2>/dev/null || true
    sleep 2

    # 3. Aggressively free the port (process may still be shutting down)
    kill_port "$PORT"
    if ! wait_for_port_free "$PORT"; then
        echo "FATAL: Cannot free port $PORT"
        return 1
    fi

    # 4. Wait for old binary to be released (file handles closed)
    if [[ -f "$dest" ]]; then
        wait_for_file_released "$dest" || true
        sleep 1
    fi

    # 5. Atomic swap with retries (handles occasional "Device or resource busy")
    if ! retry_op $FILE_OPERATION_RETRIES 2 mv -f "$dest_new" "$dest"; then
        echo "FATAL: Failed to move $dest_new to $dest (Text file busy / resource busy?)"
        return 1
    fi

    echo "  Binary deployed successfully"
    return 0
}

# Wait for service to be healthy
wait_for_healthy() {
    local port=$1
    local attempt=1

    echo "Waiting for service on port $port (max $MAX_HEALTH_ATTEMPTS attempts)..."

    while [[ $attempt -le $MAX_HEALTH_ATTEMPTS ]]; do
        if curl -sf --connect-timeout 2 "http://localhost:${port}/health" > /dev/null 2>&1; then
            echo "  Service healthy after $attempt attempts"
            return 0
        fi
        echo "  Attempt $attempt/$MAX_HEALTH_ATTEMPTS: Not ready..."
        sleep 1
        attempt=$((attempt + 1))
    done

    echo "WARNING: Health check failed after $MAX_HEALTH_ATTEMPTS attempts"
    pm2 status "$APP_NAME" 2>/dev/null || true
    return 1
}

# Parse CLI arguments
while [[ "$#" -gt 0 ]]; do
    case "$1" in
    --app-name)
        APP_NAME="$2"
        shift 2
        ;;
    --port)
        PORT="$2"
        shift 2
        ;;
    --app-dir)
        APP_DIR="$2"
        shift 2
        ;;
    --crate-name)
        CRATE_NAME="$2"
        shift 2
        ;;
    *)
        usage
        ;;
    esac
done

# Load defaults if necessary
load_defaults

# Validate inputs
if [[ -z "$APP_NAME" || -z "$PORT" || -z "$APP_DIR" ]]; then
    usage
fi

CRATE_NAME="${CRATE_NAME:-athena_rs}"
BINARY_NAME="${CRATE_NAME}"
BUILD_ARTIFACT="./target/release/${CRATE_NAME}"
DEPLOY_PATH="./${BINARY_NAME}"

echo "========================================"
echo "Redeploying $APP_NAME on port $PORT"
echo "Binary: $BINARY_NAME | Build: $BUILD_ARTIFACT"
echo "========================================"

cd "$APP_DIR" || {
    echo "Failed to cd to $APP_DIR"
    exit 1
}

echo "[1/8] Resetting local changes..."
git reset --hard || { echo "Failed to reset"; exit 1; }

echo "[2/8] Pulling latest..."
git pull origin main || { echo "Failed to pull"; exit 1; }

echo "[3/8] Building release..."
cargo build --release || { echo "Failed to build"; exit 1; }

if [[ ! -f "$BUILD_ARTIFACT" ]]; then
    echo "FATAL: Build artifact not found: $BUILD_ARTIFACT"
    exit 1
fi

echo "[4/8] Generating docs..."
cargo d 2>/dev/null || echo "  (cargo d skipped or failed, continuing)"

echo "[5/8] Atomic binary deploy (avoids Text file busy)..."
if ! atomic_binary_deploy "$BUILD_ARTIFACT" "$DEPLOY_PATH"; then
    echo "FATAL: Atomic deploy failed"
    exit 1
fi

echo "[6/8] Starting $APP_NAME with PM2..."
pm2 start "$DEPLOY_PATH" --name "$APP_NAME" -- --api-only --port "$PORT" || {
    echo "Failed to start PM2"
    exit 1
}

sleep 2

echo "[7/8] Health check..."
wait_for_healthy "$PORT" || true

echo "[8/8] Saving PM2 process list..."
pm2 save || { echo "Failed to save PM2"; exit 1; }

echo "========================================"
echo "Deployment complete for $APP_NAME"
echo "========================================"
