#!/usr/bin/env bash
# Parses the last "GPU status" table from hasher log and exposes Hive OS stats.
# Layout/order in stats JSON: temp[] -> fan[] -> hs[].  hs[] stays aligned with miner GPU rows.

miner_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[[ -f "$miner_dir/h-manifest.conf" ]] && . "$miner_dir/h-manifest.conf"

log_name="${CUSTOM_LOG_BASENAME:-/var/log/miner/hasher/hasher}.log"
config_file="${CUSTOM_CONFIG_FILENAME:-$miner_dir/hasher.conf}"

khs=0
stats='{"temp":[],"fan":[],"hs":[],"hs_units":"mhs","ar":[0,0],"bus_numbers":[]}'

clean_ansi() {
  # Remove ANSI escape sequences if the miner prints colored/status output.
  sed -r 's/\x1B\[[0-9;?]*[ -/]*[@-~]//g'
}

trim_spaces() {
  local s="${1:-}"
  s="${s#"${s%%[![:space:]]*}"}"
  s="${s%"${s##*[![:space:]]}"}"
  printf '%s' "$s"
}

num_or_zero() {
  local s
  s="$(printf '%s' "${1:-}" | tr -d ' ' | sed -E 's/,/./g; s/[^0-9.\-].*$//; s/[^0-9.\-]//g')"
  if [[ -z "$s" || "$s" == "-" || "$s" == "." ]]; then
    printf '0'
  else
    awk -v v="$s" 'BEGIN { l=tolower(v); if (v == "" || l ~ /nan|inf/) print 0; else printf "%.0f", v }' 2>/dev/null || printf '0'
  fi
}

num_float_or_zero() {
  local s
  s="$(printf '%s' "${1:-}" | tr -d ' ' | sed -E 's/,/./g; s/[^0-9.\-].*$//; s/[^0-9.\-]//g')"
  if [[ -z "$s" || "$s" == "-" || "$s" == "." ]]; then
    printf '0'
  else
    awk -v v="$s" 'BEGIN { l=tolower(v); if (v == "" || l ~ /nan|inf/) print 0; else printf "%.3f", v }' 2>/dev/null || printf '0'
  fi
}

json_array_numbers() {
  if [[ $# -eq 0 ]]; then
    printf '[]'
    return
  fi
  local first=1 item
  printf '['
  for item in "$@"; do
    [[ $first -eq 0 ]] && printf ','
    first=0
    # Make sure JSON never receives empty/invalid numeric values.
    if [[ "$item" =~ ^-?[0-9]+([.][0-9]+)?$ ]]; then
      printf '%s' "$item"
    else
      printf '0'
    fi
  done
  printf ']'
}

bus_hex_to_dec() {
  local pci="${1:-}" bus=""
  pci="$(trim_spaces "$pci")"
  # NVIDIA can return 00000000:04:00.0 or 0000:04:00.0. Hive often stores 0000:04:00.0.
  if [[ "$pci" =~ ^[0-9A-Fa-f]{8}:([0-9A-Fa-f]{2}):[0-9A-Fa-f]{2}[.] ]]; then
    bus="${BASH_REMATCH[1]}"
  elif [[ "$pci" =~ ^[0-9A-Fa-f]{4}:([0-9A-Fa-f]{2}):[0-9A-Fa-f]{2}[.] ]]; then
    bus="${BASH_REMATCH[1]}"
  elif [[ "$pci" =~ ^([0-9A-Fa-f]{2}):[0-9A-Fa-f]{2}[.] ]]; then
    bus="${BASH_REMATCH[1]}"
  fi
  [[ -n "$bus" ]] && printf "%d" "0x$bus" 2>/dev/null
}

get_bus_numbers() {
  local file="/var/run/hive/gpu-detect.json"
  if [[ -f "$file" ]] && command -v jq >/dev/null 2>&1; then
    # Hive usually stores PCI IDs as domain:bus:device.func, for example 0000:04:00.0.
    # Use the real bus byte; otherwise several GPUs can collapse to bus 0 and the last GPU disappears.
    jq -r '.. | objects | (.busid? // .bus_id? // .pci_bus_id? // empty)' "$file" 2>/dev/null \
      | awk -F: '
          NF >= 3 { print $2; next }  # domain:bus:device.func
          NF == 2 { print $1; next }  # bus:device.func
          NF == 1 && length($1) { print $1 }
        ' \
      | sed -E 's/[^0-9A-Fa-f].*$//' \
      | while read -r b; do
          [[ -n "$b" ]] && printf "%d\n" "0x$b" 2>/dev/null
        done \
      | jq -cs '.' 2>/dev/null
  else
    printf '[]'
  fi
}

hash_to_mhs() {
  local hash_val="$1"
  local hash_unit="${2,,}"
  local mult

  case "$hash_unit" in
    h/s)  mult="0.000001" ;;
    kh/s) mult="0.001" ;;
    mh/s) mult="1" ;;
    gh/s) mult="1000" ;;
    th/s) mult="1000000" ;;
    ph/s) mult="1000000000" ;;
    *)    mult="1" ;;
  esac

  awk -v h="$hash_val" -v m="$mult" 'BEGIN { printf "%.6f", h*m }'
}

# Sensor arrays by vendor GPU index and by PCI bus. Bash associative arrays are supported in Hive OS bash.
declare -a nvidia_temp_by_idx nvidia_fan_by_idx nvidia_power_by_idx nvidia_bus_by_idx
if (( BASH_VERSINFO[0] >= 4 )); then
  declare -A temp_by_bus fan_by_bus power_by_bus
fi

read_nvidia_sensors() {
  command -v nvidia-smi >/dev/null 2>&1 || return 0
  local line idx pci temp fan pwr bus
  # fan.speed and power.draw can be "[Not Supported]" on some cards; num_* turns that into 0.
  while IFS= read -r line; do
    [[ -z "$line" ]] && continue
    IFS=',' read -r idx pci temp fan pwr <<< "$line"
    idx="$(num_or_zero "$idx")"
    temp="$(num_or_zero "$temp")"
    fan="$(num_or_zero "$fan")"
    pwr="$(num_float_or_zero "$pwr")"
    nvidia_temp_by_idx[$idx]="$temp"
    nvidia_fan_by_idx[$idx]="$fan"
    nvidia_power_by_idx[$idx]="$pwr"
    bus="$(bus_hex_to_dec "$pci")"
    [[ -n "$bus" ]] && nvidia_bus_by_idx[$idx]="$bus"
    if [[ -n "$bus" && ${BASH_VERSINFO[0]} -ge 4 ]]; then
      temp_by_bus[$bus]="$temp"
      fan_by_bus[$bus]="$fan"
      power_by_bus[$bus]="$pwr"
    fi
  done < <(nvidia-smi --query-gpu=index,pci.bus_id,temperature.gpu,fan.speed,power.draw --format=csv,noheader,nounits 2>/dev/null)
}

read_amd_sensors() {
  command -v amd-info >/dev/null 2>&1 || return 0
  local line idx bus temp fan pwr
  idx=""
  bus=""
  while IFS= read -r line; do
    line="$(printf '%s' "$line" | clean_ansi)"
    # Examples:
    # === GPU 1, 07:00.0 Radeon RX 5700 XT 8176 MB ===
    # === GPU 4, 05:00.0 /sys/class/drm/card4 ===
    if [[ "$line" =~ GPU[[:space:]]+([0-9]+),[[:space:]]+([0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}[.][0-9]) ]]; then
      idx="${BASH_REMATCH[1]}"
      bus="$(bus_hex_to_dec "${BASH_REMATCH[2]}")"
      [[ -n "$bus" ]] && amd_bus_by_idx[$idx]="$bus"
      continue
    fi

    temp=""
    fan=""
    pwr=""

    # Newer amd-info: Core: 44°C, HotSpot: 51°C, Mem: 72°C, Fan: 60%, RPM: 2070
    if [[ "$line" =~ Core:[[:space:]]*([0-9]+([.][0-9]+)?)[^,]*,[[:space:]]*HotSpot:.*Fan:[[:space:]]*([0-9]+([.][0-9]+)?)[[:space:]]*% ]]; then
      temp="${BASH_REMATCH[1]}"
      fan="${BASH_REMATCH[3]}"
    # Older amd-info: Load: 100%, Temp: 40 C, Fan: 22.7451%
    elif [[ "$line" =~ Temp:[[:space:]]*([0-9]+([.][0-9]+)?)[^,]*,[[:space:]]*Fan:[[:space:]]*([0-9]+([.][0-9]+)?)[[:space:]]*% ]]; then
      temp="${BASH_REMATCH[1]}"
      fan="${BASH_REMATCH[3]}"
    fi

    if [[ "$line" =~ Power:[[:space:]]*([0-9]+([.][0-9]+)?)[[:space:]]*W ]]; then
      pwr="${BASH_REMATCH[1]}"
    elif [[ "$line" =~ PWR[[:space:]]+([0-9]+([.][0-9]+)?)[[:space:]]*W ]]; then
      pwr="${BASH_REMATCH[1]}"
    fi

    if [[ -n "$idx" && ( -n "$temp" || -n "$fan" || -n "$pwr" ) ]]; then
      [[ -n "$temp" ]] && amd_temp_by_idx[$idx]="$(num_or_zero "$temp")"
      [[ -n "$fan" ]] && amd_fan_by_idx[$idx]="$(num_or_zero "$fan")"
      [[ -n "$pwr" ]] && amd_power_by_idx[$idx]="$(num_float_or_zero "$pwr")"
      if [[ -n "$bus" && ${BASH_VERSINFO[0]} -ge 4 ]]; then
        [[ -n "$temp" ]] && temp_by_bus[$bus]="$(num_or_zero "$temp")"
        [[ -n "$fan" ]] && fan_by_bus[$bus]="$(num_or_zero "$fan")"
        [[ -n "$pwr" ]] && power_by_bus[$bus]="$(num_float_or_zero "$pwr")"
      fi
    fi
  done < <(amd-info 2>/dev/null)
}

declare -a amd_temp_by_idx amd_fan_by_idx amd_power_by_idx amd_bus_by_idx
read_nvidia_sensors
read_amd_sensors

sensor_value_for_row() {
  local kind="$1" backend="$2" gpu_id="$3" line="$4" fallback_idx="$5" bus="${6:-}"
  local value=""

  if [[ -n "$bus" && ${BASH_VERSINFO[0]} -ge 4 ]]; then
    case "$kind" in
      temp) value="${temp_by_bus[$bus]:-}" ;;
      fan) value="${fan_by_bus[$bus]:-}" ;;
      power) value="${power_by_bus[$bus]:-}" ;;
    esac
  fi

  if [[ -z "$value" ]]; then
    if [[ "$backend" =~ ^(CUDA|NVIDIA)$ || "$line" =~ (NVIDIA|GeForce|RTX|GTX|CMP|Quadro|Tesla) ]]; then
      case "$kind" in
        temp) value="${nvidia_temp_by_idx[$gpu_id]:-${nvidia_temp_by_idx[$fallback_idx]:-}}" ;;
        fan) value="${nvidia_fan_by_idx[$gpu_id]:-${nvidia_fan_by_idx[$fallback_idx]:-}}" ;;
        power) value="${nvidia_power_by_idx[$gpu_id]:-${nvidia_power_by_idx[$fallback_idx]:-}}" ;;
      esac
    elif [[ "$backend" =~ ^(AMD|ROCM)$ || "$line" =~ (AMD|Radeon|Vega|Navi|Ellesmere|Polaris) ]]; then
      case "$kind" in
        temp) value="${amd_temp_by_idx[$gpu_id]:-${amd_temp_by_idx[$fallback_idx]:-}}" ;;
        fan) value="${amd_fan_by_idx[$gpu_id]:-${amd_fan_by_idx[$fallback_idx]:-}}" ;;
        power) value="${amd_power_by_idx[$gpu_id]:-${amd_power_by_idx[$fallback_idx]:-}}" ;;
      esac
    fi
  fi

  [[ -z "$value" ]] && value="0"
  printf '%s' "$value"
}


# Read explicit --device list from Hive extra args. This avoids mapping sensors to
# unused adapters/iGPUs when the miner is started only on a subset, for example
# --device 1,2 while nvidia-smi/amd-info still lists GPU 0.
declare -a selected_device_ids
read_selected_devices() {
  selected_device_ids=()
  local cmd dev item
  cmd="$(cat "$config_file" 2>/dev/null || true)"

  if [[ "$cmd" =~ --device=([^[:space:]]+) ]]; then
    dev="${BASH_REMATCH[1]}"
  elif [[ "$cmd" =~ --device[[:space:]]+([^[:space:]]+) ]]; then
    dev="${BASH_REMATCH[1]}"
  else
    return 0
  fi

  dev="${dev//\'/}"
  dev="${dev//\"/}"
  dev="${dev,,}"
  [[ -z "$dev" || "$dev" == "all" || "$dev" == "auto" ]] && return 0

  IFS=',' read -ra selected_device_ids <<< "$dev"
  local cleaned=()
  for item in "${selected_device_ids[@]}"; do
    item="$(trim_spaces "$item")"
    [[ "$item" =~ ^[0-9]+$ ]] && cleaned+=("$item")
  done
  selected_device_ids=("${cleaned[@]}")
}

map_device_id_for_row() {
  local row_idx="$1" gpu_id="$2"
  if [[ ${#selected_device_ids[@]} -gt 0 && -n "${selected_device_ids[$row_idx]:-}" ]]; then
    printf '%s' "${selected_device_ids[$row_idx]}"
  else
    printf '%s' "$gpu_id"
  fi
}

sensor_bus_for_row() {
  local backend="$1" gpu_id="$2" line="$3" fallback_idx="$4" row_bus="${5:-}" value=""
  if [[ -n "$row_bus" ]]; then
    printf '%s' "$row_bus"
    return 0
  fi

  if [[ "$backend" =~ ^(CUDA|NVIDIA)$ || "$line" =~ (NVIDIA|GeForce|RTX|GTX|CMP|Quadro|Tesla) ]]; then
    value="${nvidia_bus_by_idx[$gpu_id]:-${nvidia_bus_by_idx[$fallback_idx]:-}}"
  elif [[ "$backend" =~ ^(AMD|ROCM)$ || "$line" =~ (AMD|Radeon|Vega|Navi|Ellesmere|Polaris) ]]; then
    value="${amd_bus_by_idx[$gpu_id]:-${amd_bus_by_idx[$fallback_idx]:-}}"
  fi
  printf '%s' "$value"
}

read_selected_devices

if [[ -f "$log_name" ]]; then
  # Keep only the most recent GPU status table from the end of the log.
  block="$(
    tail -n 400 "$log_name" 2>/dev/null | clean_ansi | awk '
      /GPU status/ { buf=""; grab=1 }
      grab { buf = buf $0 ORS }
      END { printf "%s", buf }
    '
  )"

  hs=()
  temp=()
  fan=()
  power=()
  bus_numbers_rows=()
  have_power=0
  row_idx=0
  sum_mhs="0"
  acc=0
  stale=0
  rej=0

  while IFS= read -r line; do
    # Rows can be plain table lines or timestamp-prefixed log lines. Match the backend and
    # numeric GPU id anywhere on the line, but ignore the TOTAL row.
    [[ "$line" =~ (CUDA|OPENCL|OCL|ROCM|AMD|NVIDIA)[[:space:]]+([0-9]+)[[:space:]]+ ]] || continue

    backend="${BASH_REMATCH[1]}"
    gpu_id="${BASH_REMATCH[2]}"
    pwr=""
    hash_val=""
    hash_unit=""
    shares=""
    row_bus=""

    # If a future miner table prints PCI as 0000:04:00.0 or 04:00.0, use it for exact sensor mapping.
    if [[ "$line" =~ ([0-9A-Fa-f]{4}:)?([0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}[.][0-9]) ]]; then
      row_bus="$(bus_hex_to_dec "${BASH_REMATCH[0]}")"
    fi

    if [[ "$line" =~ ([0-9]+([.][0-9]+)?)W ]]; then
      pwr="${BASH_REMATCH[1]}"
      have_power=1
    fi

    # Supports both miner formats: "123.45 MH/s" and "123.45MH/s".
    if [[ "$line" =~ ([0-9]+([.][0-9]+)?)[[:space:]]*([kKmMgGtTpP]?[hH]/s) ]]; then
      hash_val="${BASH_REMATCH[1]}"
      hash_unit="${BASH_REMATCH[3]}"
    fi

    if [[ "$line" =~ ([0-9]+)/([0-9]+)/([0-9]+) ]]; then
      shares="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}/${BASH_REMATCH[3]}"
    fi

    # Keep one hashrate element per GPU row even while a device is warming up/waiting.
    # Otherwise Hive can hide the last GPU when the last row has no numeric hash yet.
    if [[ -n "$hash_val" && -n "$hash_unit" ]]; then
      hash_mhs="$(hash_to_mhs "$hash_val" "$hash_unit")"
    else
      hash_mhs="0.000000"
    fi

    hs+=("$hash_mhs")
    sum_mhs="$(awk -v a="$sum_mhs" -v b="$hash_mhs" 'BEGIN { printf "%.6f", a+b }')"

    sensor_gpu_id="$(map_device_id_for_row "$row_idx" "$gpu_id")"
    row_bus_final="$(sensor_bus_for_row "$backend" "$sensor_gpu_id" "$line" "$sensor_gpu_id" "$row_bus")"
    bus_numbers_rows+=("$row_bus_final")

    # temp/fan are above hashrate in Hive; keep them strictly aligned with hs[].
    # Use only the GPU rows printed by the miner. Extra unused/iGPU adapters from
    # nvidia-smi/amd-info are ignored, so the last GPU does not get shifted away.
    temp+=("$(sensor_value_for_row temp "$backend" "$sensor_gpu_id" "$line" "$sensor_gpu_id" "$row_bus_final")")
    fan+=("$(sensor_value_for_row fan "$backend" "$sensor_gpu_id" "$line" "$sensor_gpu_id" "$row_bus_final")")

    # Prefer miner-table power because it is the same order as hs[]; if absent, fall back to nvidia-smi/amd-info.
    if [[ -n "$pwr" ]]; then
      power+=("$pwr")
      have_power=1
    else
      sensor_pwr="$(sensor_value_for_row power "$backend" "$sensor_gpu_id" "$line" "$sensor_gpu_id" "$row_bus_final")"
      power+=("$sensor_pwr")
      [[ "$sensor_pwr" != "0" ]] && have_power=1
    fi

    if [[ "$shares" =~ ^([0-9]+)/([0-9]+)/([0-9]+)$ ]]; then
      acc=$((acc + BASH_REMATCH[1]))
      stale=$((stale + BASH_REMATCH[2]))
      rej=$((rej + BASH_REMATCH[3]))
    fi

    row_idx=$((row_idx + 1))
  done <<< "$block"

  khs="$(awk -v mhs="$sum_mhs" 'BEGIN { printf "%.0f", mhs*1000 }')"
  bad=$((stale + rej))

  hs_json="$(json_array_numbers "${hs[@]}")"
  temp_json="$(json_array_numbers "${temp[@]}")"
  fan_json="$(json_array_numbers "${fan[@]}")"
  power_json="$(json_array_numbers "${power[@]}")"
  bus_numbers="[]"
  bus_ok=1
  if [[ ${#bus_numbers_rows[@]} -gt 0 && ${#bus_numbers_rows[@]} -eq ${#hs[@]} ]]; then
    for b in "${bus_numbers_rows[@]}"; do
      [[ "$b" =~ ^[0-9]+$ ]] || { bus_ok=0; break; }
    done
    [[ "$bus_ok" -eq 1 ]] && bus_numbers="$(json_array_numbers "${bus_numbers_rows[@]}")"
  fi

  if command -v jq >/dev/null 2>&1; then
    stats="$(
      jq -nc \
        --argjson temp "$temp_json" \
        --argjson fan "$fan_json" \
        --argjson hs "$hs_json" \
        --argjson power "$power_json" \
        --argjson bus_numbers "$bus_numbers" \
        --argjson acc "$acc" \
        --argjson bad "$bad" \
        --argjson have_power "$have_power" \
        --arg algo "${CUSTOM_ALGO:-custom}" \
        --arg ver "${CUSTOM_VERSION:-unknown}" \
        '
        {
          temp: $temp,
          fan: $fan,
          hs: $hs,
          hs_units: "mhs",
          ar: [$acc, $bad],
          algo: $algo,
          ver: $ver,
          bus_numbers: $bus_numbers
        }
        + if ($have_power == 1 and ($power | length) > 0) then
            {power: $power, total_power: ($power | add)}
          else
            {}
          end
        '
    )"
  else
    stats="{\"temp\":$temp_json,\"fan\":$fan_json,\"hs\":$hs_json,\"hs_units\":\"mhs\",\"ar\":[$acc,$bad],\"algo\":\"${CUSTOM_ALGO:-custom}\",\"ver\":\"${CUSTOM_VERSION:-unknown}\",\"bus_numbers\":$bus_numbers"
    if [[ "$have_power" -eq 1 ]]; then
      total_power="$(awk -v values="${power[*]}" 'BEGIN { split(values, a, " "); s=0; for (i in a) s+=a[i]; printf "%.3f", s }')"
      stats+=",\"power\":$power_json,\"total_power\":$total_power"
    fi
    stats+="}"
  fi
fi

if [[ "${1:-}" == "--print" || "${BASH_SOURCE[0]}" == "$0" ]]; then
  echo "khs=$khs"
  echo "stats=$stats"
fi
