#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
WORKSPACE_ROOT="$(cd "$ROOT_DIR/.." && pwd)"
LINUXCNC_DIR="${LINUXCNC_DIR:-$WORKSPACE_ROOT/linuxcnc}"
SIM_DIR="$LINUXCNC_DIR/configs/sim"
BUILD_DIR="${SIM_CONFIG_BUILD_DIR:-$ROOT_DIR/build/native/sim-configs}"
RS274="${RS274:-$LINUXCNC_DIR/bin/rs274}"
TIMEOUT_SECONDS="${SIM_CONFIG_TIMEOUT_SECONDS:-30}"
ONLY_FILTER=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --only)
      [[ $# -ge 2 ]] || { echo "missing value for --only" >&2; exit 2; }
      ONLY_FILTER="$2"
      shift 2
      ;;
    --help|-h)
      cat <<EOF
Usage: $0 [--only PATH_SUBSTRING]

Runs LinuxCNC configs/sim .ngc programs through LinuxCNC's rs274 standalone
entry point and writes logs plus summary.tsv, class-summary.tsv, and
path-matrix.tsv under:
  $BUILD_DIR
EOF
      exit 0
      ;;
    *)
      echo "unknown argument: $1" >&2
      exit 2
      ;;
  esac
done

if [[ ! -x "$RS274" ]]; then
  echo "missing executable rs274: $RS274" >&2
  exit 1
fi

if [[ ! -d "$SIM_DIR" ]]; then
  echo "missing LinuxCNC sim config directory: $SIM_DIR" >&2
  exit 1
fi

mkdir -p "$BUILD_DIR"
exec 9>"$BUILD_DIR/inventory.lock"
flock 9

"$ROOT_DIR/tools/build_native_probes.sh"

export LD_LIBRARY_PATH="$LINUXCNC_DIR/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export PYTHONPATH="$LINUXCNC_DIR/lib/python${PYTHONPATH:+:$PYTHONPATH}"

mkdir -p "$BUILD_DIR"/{logs,out,wrapped,params}
: > "$BUILD_DIR/skipped.tsv"
SUMMARY_FILE="$BUILD_DIR/summary.tsv"
CLASS_SUMMARY_FILE="$BUILD_DIR/class-summary.tsv"
PATH_MATRIX_FILE="$BUILD_DIR/path-matrix.tsv"
printf 'path\tclass\tstatus\texpected_failure\texit_code\tseconds\tini\ttbl\toutput_lines\tstderr_summary\n' > "$SUMMARY_FILE"

relpath() {
  local path="$1"
  printf '%s\n' "${path#"$SIM_DIR"/}"
}

safe_name() {
  local rel="$1"
  rel="${rel//\//_}"
  rel="${rel// /_}"
  printf '%s\n' "$rel"
}

find_nearest_ini() {
  local dir="$1"
  local candidate
  while [[ "$dir" == "$SIM_DIR"* ]]; do
    candidate="$(find "$dir" -maxdepth 1 -type f -name '*.ini' | sort | sed -n '1p')"
    if [[ -n "$candidate" ]]; then
      printf '%s\n' "$candidate"
      return 0
    fi
    [[ "$dir" == "$SIM_DIR" ]] && break
    dir="$(dirname "$dir")"
  done
  return 1
}

find_ini_by_open_file() {
  local ngc="$1"
  local ngc_dir="$2"
  local ini open_file resolved
  while IFS= read -r ini; do
    open_file="$(ini_value "$ini" DISPLAY OPEN_FILE || true)"
    [[ -z "$open_file" ]] && continue
    if [[ "$open_file" = /* ]]; then
      resolved="$open_file"
    else
      resolved="$(realpath -m "$(dirname "$ini")/$open_file")"
    fi
    if [[ "$resolved" == "$(realpath -m "$ngc")" ]]; then
      printf '%s\n' "$ini"
      return 0
    fi
  done < <(find "$ngc_dir" -maxdepth 1 -type f -name '*.ini' | sort)
  return 1
}

find_sim_ini() {
  local rel="$1"
  local ngc_dir="$2"
  local ngc="$3"
  if find_ini_by_open_file "$ngc" "$ngc_dir"; then
    return 0
  fi
  case "$rel" in
    axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/*)
      printf '%s\n' "$SIM_DIR/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini"
      return 0
      ;;
  esac
  find_nearest_ini "$ngc_dir"
}

ini_value() {
  local ini="$1"
  local section="$2"
  local key="$3"
  awk -v section="$section" -v key="$key" '
    BEGIN { in_section = 0 }
    /^[[:space:]]*[#;]/ { next }
    /^[[:space:]]*\[/ {
      in_section = (toupper($0) ~ "\\[" toupper(section) "\\]")
      next
    }
    in_section {
      line = $0
      sub(/[[:space:]]*[#;].*$/, "", line)
      split(line, parts, "=")
      name = parts[1]
      gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
      if (toupper(name) == toupper(key)) {
        value = substr(line, index(line, "=") + 1)
        gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
        print value
        exit
      }
    }
  ' "$ini"
}

find_upward_file_by_name() {
  local start_dir="$1"
  local basename="$2"
  local dir="$start_dir"
  while [[ "$dir" == "$SIM_DIR"* ]]; do
    if [[ -f "$dir/$basename" ]]; then
      printf '%s\n' "$dir/$basename"
      return 0
    fi
    [[ "$dir" == "$SIM_DIR" ]] && break
    dir="$(dirname "$dir")"
  done
  return 1
}

find_nearest_tbl() {
  local ngc_dir="$1"
  local ini="$2"
  local tool_table
  tool_table="$(ini_value "$ini" EMCIO TOOL_TABLE || true)"
  if [[ -n "$tool_table" ]]; then
    if [[ "$tool_table" = /* && -f "$tool_table" ]]; then
      printf '%s\n' "$tool_table"
      return 0
    fi
    if [[ -f "$(dirname "$ini")/$tool_table" ]]; then
      printf '%s\n' "$(dirname "$ini")/$tool_table"
      return 0
    fi
    if find_upward_file_by_name "$ngc_dir" "$(basename "$tool_table")"; then
      return 0
    fi
  fi

  local dir="$ngc_dir"
  local candidate
  while [[ "$dir" == "$SIM_DIR"* ]]; do
    candidate="$(find "$dir" -maxdepth 1 -type f -name '*.tbl' | sort | sed -n '1p')"
    if [[ -n "$candidate" ]]; then
      printf '%s\n' "$candidate"
      return 0
    fi
    [[ "$dir" == "$SIM_DIR" ]] && break
    dir="$(dirname "$dir")"
  done
  return 1
}

copy_parameter_file() {
  local ini="$1"
  local dst="$2"
  local parameter_file
  parameter_file="$(ini_value "$ini" RS274NGC PARAMETER_FILE || true)"
  rm -f "$dst" "$dst.bak"
  if [[ -n "$parameter_file" ]]; then
    if [[ "$parameter_file" = /* && -f "$parameter_file" ]]; then
      cp "$parameter_file" "$dst"
      return 0
    fi
    if [[ -f "$(dirname "$ini")/$parameter_file" ]]; then
      cp "$(dirname "$ini")/$parameter_file" "$dst"
      return 0
    fi
  fi
  return 0
}

has_program_end() {
  local ngc="$1"
  awk '
    BEGIN { found = 0 }
    /^[[:space:]]*%[[:space:]]*$/ { found = 1 }
    {
      line = tolower($0)
      gsub(/\([^)]*\)/, "", line)
      if (line ~ /(^|[[:space:]])m0*(2|30)([[:space:]]|$)/) {
        found = 1
      }
    }
    END { exit(found ? 0 : 1) }
  ' "$ngc"
}

classify_program() {
  local rel="$1"
  local ngc="$2"
  if [[ "$rel" == */remap_subs/* || "$rel" == */nc_subroutines/* ]]; then
    printf 'remap_subroutine\n'
  elif [[ "$rel" == */macros/* || "$rel" == */lathe_configs/* ]]; then
    printf 'macro_load\n'
  elif has_program_end "$ngc"; then
    printf 'main\n'
  else
    printf 'macro_load\n'
  fi
}

prepare_input() {
  local class="$1"
  local ngc="$2"
  local wrapped="$3"
  if [[ "$class" == "main" ]]; then
    printf '%s\n' "$ngc"
    return 0
  fi
  cp "$ngc" "$wrapped"
  printf '\nM2\n' >> "$wrapped"
  printf '%s\n' "$wrapped"
}

known_expected_failure() {
  local rel="$1"
  local combined="$2"
  case "$rel" in
    gmoccapy/*)
      [[ "$combined" == *"python/toplevel.py"* ]] && { printf 'python-remap-runtime-edge\n'; return 0; }
      ;;
    axis/laser/*)
      [[ "$combined" == *"python/toplevel.py"* ]] && { printf 'python-remap-runtime-edge\n'; return 0; }
      ;;
    axis/external_offsets/dyn_demo.ngc|axis/external_offsets/eoffsets.ngc|axis/external_offsets/jwp_z.ngc|axis/external_offsets/opa_demo.ngc)
      [[ "$combined" == *"Unknown m code used: M111"* ]] && { printf 'user-m-code-M111\n'; return 0; }
      ;;
    axis/geometry/xyzc.ngc)
      [[ "$combined" == *"Unknown m code used: M110"* ]] && { printf 'user-m-code-M110\n'; return 0; }
      ;;
    axis/foam/foam.ngc)
      [[ "$combined" == *"Bad character 'u' used"* ]] && { printf 'ini-axis-mask-UV\n'; return 0; }
      ;;
    axis/vismach/5axis/bridgemill/5axisgui.ngc)
      [[ "$combined" == *"Bad character 'w' used"* ]] && { printf 'ini-axis-mask-W\n'; return 0; }
      ;;
    axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition_g533.ngc)
      [[ "$combined" == *"Cannot use axis values without a g code that uses them"* ]] && { printf 'upstream-demo-missing-motion-gcode\n'; return 0; }
      ;;
    axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/*)
      [[ "$combined" == *"python/toplevel.py"* ]] && { printf 'python-remap-runtime-edge\n'; return 0; }
      ;;
  esac
  return 1
}

summarize_text() {
  local file="$1"
  if [[ ! -f "$file" ]]; then
    return 0
  fi
  tr '\n\t' '  ' < "$file" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//' | cut -c 1-260
}

total=0
pass=0
fail=0
timeout_count=0
skip=0
expected_fail=0
unexpected_fail=0

while IFS= read -r ngc; do
  rel="$(relpath "$ngc")"
  if [[ -n "$ONLY_FILTER" && "$rel" != *"$ONLY_FILTER"* ]]; then
    continue
  fi
  if [[ "$rel" == */python/*.ngc ]]; then
    printf '%s\tunsupported_runtime_edge\tpython-ngc-helper\n' "$rel" >> "$BUILD_DIR/skipped.tsv"
    skip=$((skip + 1))
    continue
  fi

  ini="$(find_sim_ini "$rel" "$(dirname "$ngc")" "$ngc" || true)"
  if [[ -z "$ini" ]]; then
    printf '%s\tunsupported_runtime_edge\tmissing-ini\n' "$rel" >> "$BUILD_DIR/skipped.tsv"
    skip=$((skip + 1))
    continue
  fi
  tbl="$(find_nearest_tbl "$(dirname "$ngc")" "$ini" || true)"
  if [[ -z "$tbl" ]]; then
    printf '%s\tunsupported_runtime_edge\tmissing-tool-table\n' "$rel" >> "$BUILD_DIR/skipped.tsv"
    skip=$((skip + 1))
    continue
  fi

  total=$((total + 1))
  class="$(classify_program "$rel" "$ngc")"
  safe="$(safe_name "$rel")"
  wrapped="$BUILD_DIR/wrapped/$safe"
  input="$(prepare_input "$class" "$ngc" "$wrapped")"
  out_file="$BUILD_DIR/out/$safe.out"
  stdout_file="$BUILD_DIR/logs/$safe.stdout"
  stderr_file="$BUILD_DIR/logs/$safe.stderr"
  param_file="$BUILD_DIR/params/$safe.var"
  copy_parameter_file "$ini" "$param_file"

  start_time="$(date +%s)"
  set +e
  (
    cd "$(dirname "$ini")"
    INI_FILE_NAME="$ini" timeout "$TIMEOUT_SECONDS" "$RS274" \
      -g \
      -i "$(basename "$ini")" \
      -t "$tbl" \
      -v "$param_file" \
      "$input" \
      "$out_file"
  ) >"$stdout_file" 2>"$stderr_file"
  rc=$?
  set -e
  end_time="$(date +%s)"
  seconds=$((end_time - start_time))

  status="FAIL"
  if [[ "$rc" -eq 0 ]]; then
    status="PASS"
    pass=$((pass + 1))
  elif [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then
    status="TIMEOUT"
    timeout_count=$((timeout_count + 1))
  else
    fail=$((fail + 1))
  fi

  combined="$(cat "$stdout_file" "$stderr_file" 2>/dev/null || true)"
  expected=""
  if [[ "$status" != "PASS" ]]; then
    expected="$(known_expected_failure "$rel" "$combined" || true)"
    if [[ -n "$expected" ]]; then
      expected_fail=$((expected_fail + 1))
    else
      unexpected_fail=$((unexpected_fail + 1))
    fi
  fi

  output_lines=0
  [[ -f "$out_file" ]] && output_lines="$(wc -l < "$out_file" | tr -d '[:space:]')"
  stderr_summary="$(summarize_text "$stderr_file")"
  printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
    "$rel" \
    "$class" \
    "$status" \
    "$expected" \
    "$rc" \
    "$seconds" \
    "$(relpath "$ini")" \
    "$(relpath "$tbl")" \
    "$output_lines" \
    "$stderr_summary" >> "$SUMMARY_FILE"
done < <(find "$SIM_DIR" -type f -name '*.ngc' | sort)

{
  printf 'class\tstatus\texpected_failure\tcount\n'
  awk -F '\t' '
    NR > 1 {
      class[$2] += 1
      status[$2 "\t" $3] += 1
      if ($4 != "") {
        expected[$2 "\t" $4] += 1
      }
    }
    END {
      for (key in class) {
        print key "\tALL\t-\t" class[key]
      }
      for (key in status) {
        split(key, parts, "\t")
        print parts[1] "\t" parts[2] "\t-\t" status[key]
      }
      for (key in expected) {
        split(key, parts, "\t")
        print parts[1] "\tEXPECTED_FAIL\t" parts[2] "\t" expected[key]
      }
    }
  ' "$SUMMARY_FILE" | sort
} > "$CLASS_SUMMARY_FILE"

awk -F '\t' '
  BEGIN {
    OFS = "\t"
    print "path", "class", "status", "expected_failure", "ini", "tbl", "runtime_family", "blocked"
  }
  NR > 1 {
    runtime_family = $1
    sub("/[^/]+$", "", runtime_family)
    blocked = ""
    if ($1 ~ /^axis\/db_demo\//) {
      blocked = "L4-TOOL-DB"
    } else if ($1 ~ /^axis\/vismach\/millturn\// && $2 == "main") {
      blocked = "L4-USER-M-PROCESS"
    } else if ($4 == "upstream-demo-missing-motion-gcode") {
      blocked = "UPSTREAM-DEMO"
    } else if ($1 ~ /^axis\/remap\// && $2 == "main") {
      blocked = "L4-PYTHON-REMAP"
    } else if ($1 ~ /^axis\/vismach\/VMC_toolchange\// && $2 == "main") {
      blocked = "L4-PYTHON-REMAP"
    } else if ($1 ~ /^gmoccapy\//) {
      blocked = "L4-PYTHON-REMAP"
    } else if ($1 ~ /^axis\/laser\//) {
      blocked = "L4-PYTHON-REMAP"
    } else if ($1 ~ /^axis\/vismach\/5axis\/table-rotary_spindle-rotary-nutating\/demos\//) {
      blocked = "L4-PYTHON-REMAP"
    } else if ($2 == "remap_subroutine") {
      blocked = "ASSET-ONLY"
    }
    print $1, $2, $3, ($4 == "" ? "-" : $4), $7, $8, runtime_family, (blocked == "" ? "-" : blocked)
  }
' "$SUMMARY_FILE" > "$PATH_MATRIX_FILE"

{
  printf 'sim config harness summary\n'
  printf 'summary: %s\n' "$SUMMARY_FILE"
  printf 'class_summary: %s\n' "$CLASS_SUMMARY_FILE"
  printf 'path_matrix: %s\n' "$PATH_MATRIX_FILE"
  printf 'total: %s\n' "$total"
  printf 'pass: %s\n' "$pass"
  printf 'fail: %s\n' "$fail"
  printf 'timeout: %s\n' "$timeout_count"
  printf 'expected_fail: %s\n' "$expected_fail"
  printf 'unexpected_fail: %s\n' "$unexpected_fail"
  printf 'skipped: %s\n' "$skip"
} | tee "$BUILD_DIR/summary.txt"

if [[ "$unexpected_fail" -ne 0 ]]; then
  echo "unexpected sim-config failures:" >&2
  awk -F '\t' 'NR > 1 && $3 != "PASS" && $4 == "" { print $1 "\t" $3 "\t" $10 }' "$SUMMARY_FILE" >&2
  exit 1
fi
