#!/usr/bin/env bash
#
# Promtail to Alloy Config Converter
#
# Parses an existing Promtail YAML configuration and outputs equivalent
# Grafana Alloy River syntax. Requires yq for YAML parsing.
#
# Usage:
#   ./promtail-to-alloy.sh promtail.yml
#   ./promtail-to-alloy.sh promtail.yml -o config.alloy
#   ./promtail-to-alloy.sh --input /etc/promtail/config.yml --output /etc/alloy/config.alloy
#
# Author:  Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
# Version: 1.0

set -euo pipefail

# Constants
VERSION="1.0"
SCRIPT_NAME="$(basename "$0")"

# Defaults
INPUT=""
OUTPUT=""
CUSTOM_HOSTNAME=""
DRY_RUN=false

# Counters for summary
COUNT_SCRAPE_CONFIGS=0
COUNT_PIPELINE_STAGES=0
COUNT_RELABEL_RULES=0

# --- Functions ---

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

Promtail to Alloy Config Converter v${VERSION}

Reads a Promtail YAML configuration file and outputs equivalent Grafana Alloy
River syntax to stdout or a file.

Options:
  -i, --input FILE      Promtail YAML config file (or pass as positional arg)
  -o, --output FILE     Output file (default: stdout)
  --hostname NAME       Override hostname label (default: extracted from config
                        or \$(hostname -s))
  --dry-run             Parse and show what would be converted without generating
  -h, --help            Show this help
  --version             Show version

Examples:
  $SCRIPT_NAME promtail.yml
  $SCRIPT_NAME promtail.yml -o config.alloy
  $SCRIPT_NAME --input /etc/promtail/config.yml --output /etc/alloy/config.alloy
  $SCRIPT_NAME --dry-run promtail.yml
EOF
    exit 0
}

check_deps() {
    if ! command -v yq &>/dev/null; then
        echo "ERROR: yq is required but not installed." >&2
        echo "" >&2
        echo "Install yq (mikefarah version):" >&2
        echo "  # Binary install:" >&2
        echo "  sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" >&2
        echo "  sudo chmod +x /usr/local/bin/yq" >&2
        echo "" >&2
        echo "  # Or via snap:" >&2
        echo "  sudo snap install yq" >&2
        echo "" >&2
        echo "  See: https://github.com/mikefarah/yq" >&2
        exit 1
    fi
}

sanitize_label() {
    local raw="$1"
    echo "$raw" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_]/_/g' | sed 's/^_//;s/_$//' | sed 's/__*/_/g'
}

escape_river_string() {
    local raw="$1"
    # Double backslashes for River string literals
    echo "$raw" | sed 's/\\/\\\\/g'
}

generate_header() {
    local input_file="$1"
    local input_basename
    input_basename="$(basename "$input_file")"

    echo "// Grafana Alloy Configuration"
    echo "// Converted from Promtail config: ${input_basename}"
    echo "// Date: $(date +%Y-%m-%d)"
    echo "// Converter: ${SCRIPT_NAME} v${VERSION}"
    echo ""
}

extract_clients() {
    local file="$1"
    local client_count
    client_count=$(yq '.clients | length' "$file" 2>/dev/null || echo "0")

    if [[ "$client_count" -eq 0 ]]; then
        echo >&2 "WARN: No clients found in config, using placeholder URL"
        echo "// Loki endpoint"
        echo 'loki.write "default" {'
        echo '    endpoint {'
        echo '        url = "http://loki:3100/loki/api/v1/push"'
        echo '    }'
        echo '}'
        return
    fi

    # Use the first client URL for the default write endpoint
    local url
    url=$(yq '.clients[0].url' "$file")

    echo "// Loki endpoint"
    echo 'loki.write "default" {'
    echo '    endpoint {'
    echo "        url = \"${url}\""
    echo '    }'
    echo '}'
}

detect_hostname() {
    local file="$1"

    if [[ -n "$CUSTOM_HOSTNAME" ]]; then
        echo "$CUSTOM_HOSTNAME"
        return
    fi

    # Try to extract from first scrape_config's static_configs labels
    local host
    host=$(yq '.scrape_configs[0].static_configs[0].labels.host // ""' "$file" 2>/dev/null || echo "")
    if [[ -n "$host" && "$host" != "null" ]]; then
        echo "$host"
        return
    fi

    hostname -s 2>/dev/null || echo "server"
}

convert_relabel_configs() {
    local file="$1"
    local sc_index="$2"
    local label="$3"
    local forward_to="$4"

    local relabel_count
    relabel_count=$(yq ".scrape_configs[${sc_index}].relabel_configs | length" "$file" 2>/dev/null || echo "0")

    if [[ "$relabel_count" -eq 0 || "$relabel_count" == "null" ]]; then
        return 1
    fi

    COUNT_RELABEL_RULES=$((COUNT_RELABEL_RULES + relabel_count))

    echo ""
    echo "// Relabel rules: ${label}"
    echo "loki.relabel \"${label}\" {"
    echo "    forward_to = [${forward_to}]"

    for ((r = 0; r < relabel_count; r++)); do
        echo ""
        echo "    rule {"

        # source_labels
        local sl_count
        sl_count=$(yq ".scrape_configs[${sc_index}].relabel_configs[${r}].source_labels | length" "$file" 2>/dev/null || echo "0")
        if [[ "$sl_count" -gt 0 && "$sl_count" != "null" ]]; then
            local sl_arr=""
            for ((s = 0; s < sl_count; s++)); do
                local sl_val
                sl_val=$(yq ".scrape_configs[${sc_index}].relabel_configs[${r}].source_labels[${s}]" "$file")
                if [[ -n "$sl_arr" ]]; then
                    sl_arr+=", "
                fi
                sl_arr+="\"${sl_val}\""
            done
            echo "        source_labels = [${sl_arr}]"
        fi

        # target_label
        local tl
        tl=$(yq ".scrape_configs[${sc_index}].relabel_configs[${r}].target_label // \"\"" "$file")
        if [[ -n "$tl" && "$tl" != "null" ]]; then
            echo "        target_label  = \"${tl}\""
        fi

        # regex
        local rx
        rx=$(yq ".scrape_configs[${sc_index}].relabel_configs[${r}].regex // \"\"" "$file")
        if [[ -n "$rx" && "$rx" != "null" ]]; then
            rx=$(escape_river_string "$rx")
            echo "        regex         = \"${rx}\""
        fi

        # action
        local act
        act=$(yq ".scrape_configs[${sc_index}].relabel_configs[${r}].action // \"\"" "$file")
        if [[ -n "$act" && "$act" != "null" ]]; then
            echo "        action        = \"${act}\""
        fi

        # replacement
        local repl
        repl=$(yq ".scrape_configs[${sc_index}].relabel_configs[${r}].replacement // \"\"" "$file")
        if [[ -n "$repl" && "$repl" != "null" ]]; then
            repl=$(escape_river_string "$repl")
            echo "        replacement   = \"${repl}\""
        fi

        echo "    }"
    done

    echo "}"
    return 0
}

convert_pipeline_stages() {
    local file="$1"
    local sc_index="$2"
    local label="$3"

    local stage_count
    stage_count=$(yq ".scrape_configs[${sc_index}].pipeline_stages | length" "$file" 2>/dev/null || echo "0")

    if [[ "$stage_count" -eq 0 || "$stage_count" == "null" ]]; then
        return 1
    fi

    COUNT_PIPELINE_STAGES=$((COUNT_PIPELINE_STAGES + stage_count))

    echo ""
    echo "// Pipeline: ${label}"
    echo "loki.process \"${label}\" {"
    echo "    forward_to = [loki.write.default.receiver]"

    for ((p = 0; p < stage_count; p++)); do
        # Determine stage type — yq returns the first key
        local stage_type
        stage_type=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}] | keys | .[0]" "$file")

        echo ""
        case "$stage_type" in
            regex)
                local expr
                expr=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].regex.expression" "$file")
                expr=$(escape_river_string "$expr")
                echo "    stage.regex {"
                echo "        expression = \"${expr}\""
                echo "    }"
                ;;
            json)
                echo "    stage.json {"
                echo "        expressions = {"
                local json_keys
                json_keys=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].json.expressions | keys | .[]" "$file" 2>/dev/null || true)
                if [[ -n "$json_keys" ]]; then
                    while IFS= read -r key; do
                        local val
                        val=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].json.expressions.${key}" "$file")
                        if [[ "$val" == "null" || -z "$val" ]]; then
                            echo "            ${key} = \"\","
                        else
                            echo "            ${key} = \"${val}\","
                        fi
                    done <<< "$json_keys"
                fi
                echo "        }"
                echo "    }"
                ;;
            logfmt)
                echo "    stage.logfmt {}"
                ;;
            labels)
                echo "    stage.labels {"
                echo "        values = {"
                local lbl_keys
                lbl_keys=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].labels | keys | .[]" "$file" 2>/dev/null || true)
                if [[ -n "$lbl_keys" ]]; then
                    while IFS= read -r key; do
                        local val
                        val=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].labels.${key} // \"\"" "$file")
                        if [[ "$val" == "null" ]]; then
                            val=""
                        fi
                        echo "            ${key} = \"${val}\","
                    done <<< "$lbl_keys"
                fi
                echo "        }"
                echo "    }"
                ;;
            timestamp)
                local ts_source ts_format
                ts_source=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].timestamp.source" "$file")
                ts_format=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].timestamp.format" "$file")
                echo "    stage.timestamp {"
                echo "        source = \"${ts_source}\""
                echo "        format = \"${ts_format}\""
                echo "    }"
                ;;
            output)
                local out_source
                out_source=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].output.source" "$file")
                echo "    stage.output {"
                echo "        source = \"${out_source}\""
                echo "    }"
                ;;
            drop)
                local drop_expr drop_reason
                drop_expr=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].drop.expression // \"\"" "$file")
                drop_reason=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].drop.drop_counter_reason // \"converted\"" "$file")
                drop_expr=$(escape_river_string "$drop_expr")
                echo "    stage.drop {"
                if [[ -n "$drop_expr" && "$drop_expr" != "null" ]]; then
                    echo "        expression         = \"${drop_expr}\""
                fi
                echo "        drop_counter_reason = \"${drop_reason}\""
                echo "    }"
                ;;
            match)
                local match_sel
                match_sel=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].match.selector" "$file")
                echo "    stage.match {"
                echo "        selector = \"${match_sel}\""
                # Nested stages in match are complex; output a placeholder comment
                local nested_count
                nested_count=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].match.stages | length" "$file" 2>/dev/null || echo "0")
                if [[ "$nested_count" -gt 0 && "$nested_count" != "null" ]]; then
                    echo "        // NOTE: ${nested_count} nested stage(s) — review and convert manually"
                fi
                echo "    }"
                ;;
            multiline)
                local ml_firstline ml_max_lines
                ml_firstline=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].multiline.firstline" "$file")
                ml_max_lines=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].multiline.max_lines // 128" "$file")
                ml_firstline=$(escape_river_string "$ml_firstline")
                echo "    stage.multiline {"
                echo "        firstline = \"${ml_firstline}\""
                echo "        max_lines = ${ml_max_lines}"
                echo "    }"
                ;;
            static_labels)
                echo "    stage.static_labels {"
                echo "        values = {"
                local sl_keys
                sl_keys=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].static_labels | keys | .[]" "$file" 2>/dev/null || true)
                if [[ -n "$sl_keys" ]]; then
                    while IFS= read -r key; do
                        local val
                        val=$(yq ".scrape_configs[${sc_index}].pipeline_stages[${p}].static_labels.${key}" "$file")
                        echo "            ${key} = \"${val}\","
                    done <<< "$sl_keys"
                fi
                echo "        }"
                echo "    }"
                ;;
            *)
                echo "    // Unsupported stage type: ${stage_type} — convert manually"
                ;;
        esac
    done

    echo "}"
    return 0
}

convert_journal_source() {
    local file="$1"
    local sc_index="$2"
    local label="$3"
    local host="$4"

    local forward_target="loki.write.default.receiver"

    # Check for relabel_configs — if present, forward through relabel first
    local has_relabel=false
    local relabel_count
    relabel_count=$(yq ".scrape_configs[${sc_index}].relabel_configs | length" "$file" 2>/dev/null || echo "0")
    if [[ "$relabel_count" -gt 0 && "$relabel_count" != "null" ]]; then
        has_relabel=true
        forward_target="loki.relabel.${label}.receiver"
    fi

    # Extract journal config
    local max_age
    max_age=$(yq ".scrape_configs[${sc_index}].journal.max_age // \"12h\"" "$file")

    echo ""
    echo "// Journal source: ${label}"
    echo "loki.source.journal \"${label}\" {"
    echo "    max_age    = \"${max_age}\""

    # Journal labels
    local jl_keys
    jl_keys=$(yq ".scrape_configs[${sc_index}].journal.labels | keys | .[]" "$file" 2>/dev/null || true)
    if [[ -n "$jl_keys" ]]; then
        echo "    labels     = {"
        while IFS= read -r key; do
            local val
            val=$(yq ".scrape_configs[${sc_index}].journal.labels.${key}" "$file")
            echo "        ${key} = \"${val}\","
        done <<< "$jl_keys"
        echo "    }"
    fi

    if [[ "$has_relabel" == true ]]; then
        echo "    relabel_rules = loki.relabel.${label}.rules"
    fi

    echo "    forward_to = [${forward_target}]"
    echo "}"

    # Generate relabel block if needed
    if [[ "$has_relabel" == true ]]; then
        convert_relabel_configs "$file" "$sc_index" "$label" "loki.write.default.receiver"
    fi
}

convert_file_source() {
    local file="$1"
    local sc_index="$2"
    local label="$3"
    local host="$4"

    local forward_target="loki.write.default.receiver"

    # Check for pipeline_stages — if present, forward through process
    local has_pipeline=false
    local stage_count
    stage_count=$(yq ".scrape_configs[${sc_index}].pipeline_stages | length" "$file" 2>/dev/null || echo "0")
    if [[ "$stage_count" -gt 0 && "$stage_count" != "null" ]]; then
        has_pipeline=true
        forward_target="loki.process.${label}.receiver"
    fi

    # Iterate static_configs
    local sc_count
    sc_count=$(yq ".scrape_configs[${sc_index}].static_configs | length" "$file" 2>/dev/null || echo "0")

    if [[ "$sc_count" -eq 0 || "$sc_count" == "null" ]]; then
        echo >&2 "WARN: scrape_config[${sc_index}] '${label}' has no static_configs, skipping"
        return
    fi

    echo ""
    echo "// File source: ${label}"
    echo "loki.source.file \"${label}\" {"
    echo "    targets = ["

    for ((t = 0; t < sc_count; t++)); do
        # Get __path__
        local path_val
        path_val=$(yq ".scrape_configs[${sc_index}].static_configs[${t}].labels.__path__ // \"\"" "$file")
        if [[ -z "$path_val" || "$path_val" == "null" ]]; then
            # Try targets array format
            path_val=$(yq ".scrape_configs[${sc_index}].static_configs[${t}].targets[0].__path__ // \"\"" "$file" 2>/dev/null || echo "")
        fi

        echo "        {"

        if [[ -n "$path_val" && "$path_val" != "null" ]]; then
            echo "            \"__path__\" = \"${path_val}\","
        fi

        # Get other labels (excluding __path__)
        local label_keys
        label_keys=$(yq ".scrape_configs[${sc_index}].static_configs[${t}].labels | keys | .[]" "$file" 2>/dev/null || true)
        if [[ -n "$label_keys" ]]; then
            while IFS= read -r lk; do
                if [[ "$lk" == "__path__" ]]; then
                    continue
                fi
                local lv
                lv=$(yq ".scrape_configs[${sc_index}].static_configs[${t}].labels.${lk}" "$file")
                # Pad alignment
                printf '            "%-8s = "%s",\n' "${lk}\"" "${lv}"
            done <<< "$label_keys"
        fi

        # Add host label if not already present
        if ! echo "$label_keys" | grep -q '^host$'; then
            printf '            "%-8s = "%s",\n' 'host"' "${host}"
        fi

        echo "        },"
    done

    echo "    ]"
    echo "    forward_to = [${forward_target}]"
    echo "}"

    # Generate pipeline_stages block if present
    if [[ "$has_pipeline" == true ]]; then
        convert_pipeline_stages "$file" "$sc_index" "$label"
    fi
}

extract_scrape_configs() {
    local file="$1"
    local host="$2"

    local sc_total
    sc_total=$(yq '.scrape_configs | length' "$file" 2>/dev/null || echo "0")

    if [[ "$sc_total" -eq 0 || "$sc_total" == "null" ]]; then
        echo >&2 "WARN: No scrape_configs found in config"
        return
    fi

    COUNT_SCRAPE_CONFIGS=$sc_total

    for ((i = 0; i < sc_total; i++)); do
        local job_name
        job_name=$(yq ".scrape_configs[${i}].job_name" "$file")
        local label
        label=$(sanitize_label "$job_name")

        # Detect journal vs file source
        local has_journal
        has_journal=$(yq ".scrape_configs[${i}].journal // \"\"" "$file" 2>/dev/null || echo "")

        if [[ -n "$has_journal" && "$has_journal" != "null" && "$has_journal" != "" ]]; then
            convert_journal_source "$file" "$i" "$label" "$host"
        else
            convert_file_source "$file" "$i" "$label" "$host"
        fi
    done
}

generate_config() {
    local file="$1"
    local host="$2"

    generate_header "$file"
    extract_clients "$file"
    extract_scrape_configs "$file" "$host"
    echo ""
}

# --- Main ---

main() {
    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -i|--input)    INPUT="$2"; shift 2 ;;
            -o|--output)   OUTPUT="$2"; shift 2 ;;
            --hostname)    CUSTOM_HOSTNAME="$2"; shift 2 ;;
            --dry-run)     DRY_RUN=true; shift ;;
            -h|--help)     usage ;;
            --version)     echo "$SCRIPT_NAME v${VERSION}"; exit 0 ;;
            -*)            echo "ERROR: Unknown option: $1" >&2; echo "" >&2; usage ;;
            *)
                # Positional argument — treat as input file
                if [[ -z "$INPUT" ]]; then
                    INPUT="$1"
                else
                    echo "ERROR: Unexpected argument: $1" >&2
                    exit 1
                fi
                shift
                ;;
        esac
    done

    # Validate input
    if [[ -z "$INPUT" ]]; then
        echo "ERROR: No input file specified" >&2
        echo "" >&2
        usage
    fi

    if [[ ! -f "$INPUT" ]]; then
        echo "ERROR: Input file not found: ${INPUT}" >&2
        exit 1
    fi

    # Check dependencies
    check_deps

    # Validate YAML
    if ! yq '.' "$INPUT" &>/dev/null; then
        echo "ERROR: Failed to parse YAML: ${INPUT}" >&2
        echo "Ensure the file is valid YAML and yq can read it." >&2
        exit 1
    fi

    # Detect hostname
    local host
    host=$(detect_hostname "$INPUT")

    # Dry-run mode
    if [[ "$DRY_RUN" == true ]]; then
        echo "Dry run — parsing ${INPUT}" >&2
        echo "" >&2

        local sc_total
        sc_total=$(yq '.scrape_configs | length' "$INPUT" 2>/dev/null || echo "0")
        local client_count
        client_count=$(yq '.clients | length' "$INPUT" 2>/dev/null || echo "0")

        echo "  Clients:        ${client_count}" >&2
        echo "  Scrape configs: ${sc_total}" >&2
        echo "  Hostname:       ${host}" >&2
        echo "" >&2

        for ((i = 0; i < sc_total; i++)); do
            local jn
            jn=$(yq ".scrape_configs[${i}].job_name" "$INPUT")
            local has_j
            has_j=$(yq ".scrape_configs[${i}].journal // \"\"" "$INPUT" 2>/dev/null || echo "")
            local src_type="file"
            if [[ -n "$has_j" && "$has_j" != "null" && "$has_j" != "" ]]; then
                src_type="journal"
            fi
            local ps_count
            ps_count=$(yq ".scrape_configs[${i}].pipeline_stages | length" "$INPUT" 2>/dev/null || echo "0")
            local rl_count
            rl_count=$(yq ".scrape_configs[${i}].relabel_configs | length" "$INPUT" 2>/dev/null || echo "0")

            echo "  [${i}] ${jn} (${src_type}) — ${ps_count} pipeline stages, ${rl_count} relabel rules" >&2
        done
        echo "" >&2
        echo "Run without --dry-run to generate the Alloy config." >&2
        exit 0
    fi

    # Generate config
    if [[ -n "$OUTPUT" ]]; then
        generate_config "$INPUT" "$host" > "$OUTPUT"
        echo >&2 "Config written to: ${OUTPUT}"
    else
        generate_config "$INPUT" "$host"
    fi

    # Summary to stderr
    echo >&2 "Converted ${COUNT_SCRAPE_CONFIGS} scrape configs, ${COUNT_PIPELINE_STAGES} pipeline stages, ${COUNT_RELABEL_RULES} relabel rules"
}

main "$@"
