#!/bin/bash

################################################
#### Grafana Backup & Restore Script        ####
#### Backup dashboards, datasources, alert  ####
#### rules, and folders via the HTTP API    ####
####                                        ####
#### Author: Phil Connor                    ####
#### Contact: contact@mylinux.work          ####
#### Version: 1.0.0.20260309               ####
################################################

set -o pipefail

SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_NAME

# Default configuration
readonly DEFAULT_BACKUP_DIR="/var/backups/grafana"
readonly DEFAULT_RETENTION_COUNT=7
readonly DEFAULT_CURL_TIMEOUT=30

# Configuration variables (can be overridden by environment)
GRAFANA_URL=${GRAFANA_URL:-}
GRAFANA_TOKEN=${GRAFANA_TOKEN:-}
BACKUP_DIR=${BACKUP_DIR:-$DEFAULT_BACKUP_DIR}
RETENTION_COUNT=${RETENTION_COUNT:-$DEFAULT_RETENTION_COUNT}

# Runtime
RUN_MODE="backup"
RESTORE_DIR=""

handle_error() {
    local exit_code=$1
    local line_number=$2
    echo "Error: $SCRIPT_NAME failed at line $line_number with exit code $exit_code" >&2
    exit "$exit_code"
}

trap 'handle_error $? $LINENO' ERR

show_help() {
    cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]

Backup and restore Grafana dashboards, datasources, alert rules, and folders
via the HTTP API. Creates timestamped backup directories with automatic retention.

OPTIONS:
    --backup            Run a full backup (default)
    --restore DIR       Restore from the specified backup directory
    --list              List available backups
    --help, -h          Show this help message

ENVIRONMENT VARIABLES:
    GRAFANA_URL         Grafana base URL (required, e.g. http://localhost:3000)
    GRAFANA_TOKEN       Grafana API token with Admin permissions (required)
    BACKUP_DIR          Root backup directory (default: $DEFAULT_BACKUP_DIR)
    RETENTION_COUNT     Number of backups to retain (default: $DEFAULT_RETENTION_COUNT)

EXAMPLES:
    GRAFANA_URL=http://localhost:3000 GRAFANA_TOKEN=glsa_xxxx $SCRIPT_NAME --backup
    GRAFANA_URL=http://localhost:3000 GRAFANA_TOKEN=glsa_xxxx $SCRIPT_NAME --restore /var/backups/grafana/20260309-143022
    GRAFANA_URL=http://localhost:3000 GRAFANA_TOKEN=glsa_xxxx $SCRIPT_NAME --list

EOF
    exit 0
}

# Parse arguments
while [[ $# -gt 0 ]]; do
    case "$1" in
        --backup)   RUN_MODE="backup"; shift ;;
        --restore)  RUN_MODE="restore"; RESTORE_DIR="$2"; shift 2 ;;
        --list)     RUN_MODE="list"; shift ;;
        --help|-h)  show_help ;;
        *) echo "Unknown option: $1" >&2; show_help ;;
    esac
done

validate_config() {
    if [[ -z "$GRAFANA_URL" ]]; then
        echo "Error: GRAFANA_URL is required" >&2
        exit 1
    fi

    if [[ -z "$GRAFANA_TOKEN" ]]; then
        echo "Error: GRAFANA_TOKEN is required" >&2
        exit 1
    fi

    # Strip trailing slash
    GRAFANA_URL="${GRAFANA_URL%/}"

    # Check dependencies
    for cmd in curl jq; do
        if ! command -v "$cmd" &>/dev/null; then
            echo "Error: $cmd is required but not installed" >&2
            exit 1
        fi
    done
}

grafana_api() {
    local method="$1"
    local endpoint="$2"
    local data="${3:-}"
    local url="${GRAFANA_URL}${endpoint}"

    local args=(
        -sf
        --max-time "$DEFAULT_CURL_TIMEOUT"
        -H "Authorization: Bearer ${GRAFANA_TOKEN}"
        -H "Content-Type: application/json"
        -H "Accept: application/json"
        -X "$method"
    )

    if [[ -n "$data" ]]; then
        args+=(-d "$data")
    fi

    curl "${args[@]}" "$url"
}

backup_dashboards() {
    local dest="$1/dashboards"
    mkdir -p "$dest"

    echo "Backing up dashboards..."

    local search_result
    search_result=$(grafana_api GET "/api/search?type=dash-db&limit=5000") || {
        echo "  Error: Failed to search dashboards" >&2
        return 1
    }

    local count
    count=$(echo "$search_result" | jq 'length')
    echo "  Found $count dashboards"

    echo "$search_result" | jq -r '.[].uid' | while IFS= read -r uid; do
        local dashboard
        dashboard=$(grafana_api GET "/api/dashboards/uid/$uid") || {
            echo "  Warning: Failed to export dashboard $uid" >&2
            continue
        }

        local title
        title=$(echo "$dashboard" | jq -r '.dashboard.title // "unknown"' | tr '/ ' '__')
        echo "$dashboard" | jq '.' > "$dest/${uid}_${title}.json"
    done

    echo "  Dashboards saved to $dest"
}

backup_datasources() {
    local dest="$1/datasources"
    mkdir -p "$dest"

    echo "Backing up datasources..."

    local result
    result=$(grafana_api GET "/api/datasources") || {
        echo "  Error: Failed to fetch datasources" >&2
        return 1
    }

    local count
    count=$(echo "$result" | jq 'length')
    echo "  Found $count datasources"

    echo "$result" | jq -c '.[]' | while IFS= read -r ds; do
        local id name
        id=$(echo "$ds" | jq -r '.id')
        name=$(echo "$ds" | jq -r '.name' | tr '/ ' '__')
        echo "$ds" | jq '.' > "$dest/${id}_${name}.json"
    done

    echo "  Datasources saved to $dest"
}

backup_alert_rules() {
    local dest="$1/alert_rules"
    mkdir -p "$dest"

    echo "Backing up alert rules..."

    local result
    result=$(grafana_api GET "/api/v1/provisioning/alert-rules") || {
        echo "  Error: Failed to fetch alert rules" >&2
        return 1
    }

    local count
    count=$(echo "$result" | jq 'length')
    echo "  Found $count alert rules"

    echo "$result" | jq -c '.[]' | while IFS= read -r rule; do
        local uid title
        uid=$(echo "$rule" | jq -r '.uid // .id // "unknown"')
        title=$(echo "$rule" | jq -r '.title // "unknown"' | tr '/ ' '__')
        echo "$rule" | jq '.' > "$dest/${uid}_${title}.json"
    done

    echo "  Alert rules saved to $dest"
}

backup_folders() {
    local dest="$1/folders"
    mkdir -p "$dest"

    echo "Backing up folders..."

    local result
    result=$(grafana_api GET "/api/folders?limit=1000") || {
        echo "  Error: Failed to fetch folders" >&2
        return 1
    }

    local count
    count=$(echo "$result" | jq 'length')
    echo "  Found $count folders"

    echo "$result" | jq -c '.[]' | while IFS= read -r folder; do
        local uid title
        uid=$(echo "$folder" | jq -r '.uid')
        title=$(echo "$folder" | jq -r '.title' | tr '/ ' '__')
        echo "$folder" | jq '.' > "$dest/${uid}_${title}.json"
    done

    echo "  Folders saved to $dest"
}

restore_dashboards() {
    local src="$1/dashboards"

    if [[ ! -d "$src" ]]; then
        echo "  No dashboards directory found, skipping" >&2
        return 0
    fi

    echo "Restoring dashboards..."

    local count=0
    for file in "$src"/*.json; do
        [[ -f "$file" ]] || continue

        local payload
        payload=$(jq '{dashboard: .dashboard, overwrite: true, folderId: (.meta.folderId // 0)}' "$file") || {
            echo "  Warning: Failed to parse $file" >&2
            continue
        }

        if grafana_api POST "/api/dashboards/db" "$payload" >/dev/null; then
            count=$((count + 1))
        else
            local title
            title=$(basename "$file" .json)
            echo "  Warning: Failed to restore dashboard $title" >&2
        fi
    done

    echo "  Restored $count dashboards"
}

restore_datasources() {
    local src="$1/datasources"

    if [[ ! -d "$src" ]]; then
        echo "  No datasources directory found, skipping" >&2
        return 0
    fi

    echo "Restoring datasources..."

    local count=0
    for file in "$src"/*.json; do
        [[ -f "$file" ]] || continue

        local payload
        payload=$(jq 'del(.id, .uid, .readOnly)' "$file") || {
            echo "  Warning: Failed to parse $file" >&2
            continue
        }

        if grafana_api POST "/api/datasources" "$payload" >/dev/null; then
            count=$((count + 1))
        else
            local name
            name=$(basename "$file" .json)
            echo "  Warning: Failed to restore datasource $name" >&2
        fi
    done

    echo "  Restored $count datasources"
}

prune_backups() {
    echo "Pruning old backups (retaining $RETENTION_COUNT)..."

    local backup_count
    backup_count=$(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d | wc -l)

    if [[ $backup_count -le $RETENTION_COUNT ]]; then
        echo "  No pruning needed ($backup_count backups present)"
        return 0
    fi

    local remove_count=$((backup_count - RETENTION_COUNT))
    find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d | sort | head -n "$remove_count" | while IFS= read -r dir; do
        echo "  Removing $(basename "$dir")"
        rm -rf "$dir"
    done

    echo "  Pruned $remove_count old backups"
}

do_backup() {
    local timestamp
    timestamp=$(date +%Y%m%d-%H%M%S)
    local dest="$BACKUP_DIR/$timestamp"

    mkdir -p "$dest"
    echo "Starting backup to $dest"

    backup_dashboards "$dest"
    backup_datasources "$dest"
    backup_alert_rules "$dest"
    backup_folders "$dest"

    prune_backups

    echo "Backup complete: $dest"
}

do_restore() {
    if [[ -z "$RESTORE_DIR" ]]; then
        echo "Error: --restore requires a directory argument" >&2
        exit 1
    fi

    if [[ ! -d "$RESTORE_DIR" ]]; then
        echo "Error: Restore directory does not exist: $RESTORE_DIR" >&2
        exit 1
    fi

    echo "Restoring from $RESTORE_DIR"

    restore_dashboards "$RESTORE_DIR"
    restore_datasources "$RESTORE_DIR"

    echo "Restore complete"
}

do_list() {
    if [[ ! -d "$BACKUP_DIR" ]]; then
        echo "No backups found (directory does not exist: $BACKUP_DIR)"
        return 0
    fi

    local count=0
    for dir in "$BACKUP_DIR"/*/; do
        [[ -d "$dir" ]] || continue
        count=$((count + 1))

        local name
        name=$(basename "$dir")
        local dashboards datasources alerts folders
        dashboards=$(find "$dir/dashboards" -name '*.json' 2>/dev/null | wc -l)
        datasources=$(find "$dir/datasources" -name '*.json' 2>/dev/null | wc -l)
        alerts=$(find "$dir/alert_rules" -name '*.json' 2>/dev/null | wc -l)
        folders=$(find "$dir/folders" -name '*.json' 2>/dev/null | wc -l)

        printf "  %s  dashboards:%-4s datasources:%-4s alerts:%-4s folders:%-4s\n" \
            "$name" "$dashboards" "$datasources" "$alerts" "$folders"
    done

    if [[ $count -eq 0 ]]; then
        echo "No backups found in $BACKUP_DIR"
    else
        echo "$count backup(s) in $BACKUP_DIR"
    fi
}

main() {
    validate_config

    case "$RUN_MODE" in
        backup)  do_backup ;;
        restore) do_restore ;;
        list)    do_list ;;
    esac
}

main
