#!/usr/bin/env bash
# Upload EtherGold capture data from this Pi to Dropbox via rclone.
#
# Usage (from Pi):
#   cd ~/EtherGold/sync
#   bash install.sh                       # once — CRLF + chmod + rclone.env
#   cp rclone.env.example rclone.env        # once, then edit (if install.sh did not)
#   DRY_RUN=1 bash sync_to_dropbox.sh       # preview
#   bash sync_to_dropbox.sh                 # upload
#
# Flat layout (2026-07): bench captures under ~/EtherGold/dev/data|logs/
#   SYNC_SOURCE="dev"  (default). Field runtime capture: SYNC_SOURCE="prod".
#
# Before upload: lists every candidate under data/** and logs/** (all rollovers)
# with sizes, then total file count + bytes. Only .sync_last.log is excluded.
# After upload: reports how many were transferred vs skipped (unchanged).

if grep -q $'\r' "${BASH_SOURCE[0]}" 2>/dev/null; then
  sed -i 's/\r$//' "${BASH_SOURCE[0]}"
  exec bash "${BASH_SOURCE[0]}" "$@"
fi

set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${ROOT}/rclone.env"

if [[ -f "${ROOT}/ethergold_paths.sh" ]]; then
  # shellcheck source=ethergold_paths.sh
  source "${ROOT}/ethergold_paths.sh"
fi
ETHERGOLD_ROOT="${ETHERGOLD_ROOT:-/home/topbox/EtherGold}"
ETHERGOLD_DEV="${ETHERGOLD_DEV:-${ETHERGOLD_ROOT}/dev}"
ETHERGOLD_PROD="${ETHERGOLD_PROD:-${ETHERGOLD_ROOT}/prod}"
ETHERGOLD_LOGS="${ETHERGOLD_LOGS:-${ETHERGOLD_ROOT}/logs}"

if [[ ! -f "${ENV_FILE}" ]]; then
  echo "Missing ${ENV_FILE}" >&2
  echo "Copy rclone.env.example to rclone.env and set RCLONE_REMOTE + DROPBOX_DEST + SYNC_SOURCE." >&2
  exit 1
fi

# Windows editors often save CRLF; strip before source (avoids $'\r': command not found)
if grep -q $'\r' "${ENV_FILE}" 2>/dev/null; then
  sed -i 's/\r$//' "${ENV_FILE}"
fi

# shellcheck source=/dev/null
source "${ENV_FILE}"

if ! command -v rclone >/dev/null 2>&1; then
  echo "rclone not installed. Run: ${ROOT}/setup_rclone.sh" >&2
  exit 1
fi

RCLONE_REMOTE="${RCLONE_REMOTE:?Set RCLONE_REMOTE in rclone.env}"
DROPBOX_DEST="${DROPBOX_DEST:?Set DROPBOX_DEST in rclone.env}"

human_bytes() {
  local b="${1:-0}"
  if [[ "${b}" -lt 1024 ]]; then
    printf '%d B' "${b}"
  elif [[ "${b}" -lt $((1024 * 1024)) ]]; then
    awk -v b="${b}" 'BEGIN { printf "%.1f KiB", b / 1024 }'
  elif [[ "${b}" -lt $((1024 * 1024 * 1024)) ]]; then
    awk -v b="${b}" 'BEGIN { printf "%.2f MiB", b / (1024 * 1024) }'
  else
    awk -v b="${b}" 'BEGIN { printf "%.2f GiB", b / (1024 * 1024 * 1024) }'
  fi
}

capture_candidate_dirs() {
  printf '%s\n' \
    "${ETHERGOLD_DEV}" \
    "${ETHERGOLD_PROD}" \
    "${ETHERGOLD_LOGS}"
}

# Emit relative paths (no leading ./) matching upload_capture.rclone-filter.
# All data/** and logs/** (incl. every rollover); skip only .sync_last.log.
list_capture_relpaths() {
  local base="${1}"
  (
    cd "${base}" || exit 0
    if [[ -d data ]]; then
      find data -type f 2>/dev/null
    fi
    if [[ -d logs ]]; then
      find logs -type f ! -name '.sync_last.log' 2>/dev/null
    fi
    if [[ -d prod-data ]]; then
      find prod-data -type f 2>/dev/null
    fi
    # Flat / prod-ish files sometimes sit at SYNC_SOURCE root.
    for rel in errors.jsonl supervisor.log disable_eth_eee.log; do
      [[ -f "${rel}" ]] && printf '%s\n' "${rel}"
    done
    shopt -s nullglob
    for rel in errors.jsonl.* supervisor.log.* disable_eth_eee.log.*; do
      [[ -f "${rel}" ]] && printf '%s\n' "${rel}"
    done
    shopt -u nullglob
  ) | sed 's|^\./||' | LC_ALL=C sort -u
}

# Prod SYNC_SOURCE is often ~/EtherGold/logs — upload everything there except marker.
list_prod_relpaths() {
  local base="${1}"
  (
    cd "${base}" || exit 0
    find . -type f ! -name '.sync_last.log' ! -path './.sync_last.log' 2>/dev/null \
      | sed 's|^\./||'
  ) | LC_ALL=C sort -u
}

count_capture_files() {
  local base="${1}"
  list_capture_relpaths "${base}" | wc -l | tr -d ' '
}

file_size_bytes() {
  local path="${1}"
  if stat -c '%s' "${path}" >/dev/null 2>&1; then
    stat -c '%s' "${path}"
  else
    # macOS / BSD fallback (unused on Pi, but harmless)
    stat -f '%z' "${path}"
  fi
}

# Print inventory; sets globals INV_COUNT INV_BYTES INV_LIST_FILE
print_upload_inventory() {
  local base="${1}"
  local mode="${2}" # capture | prod
  local rel path sz width=0
  local list_tmp

  list_tmp="$(mktemp)"
  if [[ "${mode}" == "prod" ]]; then
    list_prod_relpaths "${base}" >"${list_tmp}"
  else
    list_capture_relpaths "${base}" >"${list_tmp}"
  fi

  INV_COUNT=0
  INV_BYTES=0
  while IFS= read -r rel; do
    [[ -n "${rel}" ]] || continue
    path="${base}/${rel}"
    [[ -f "${path}" ]] || continue
    INV_COUNT=$((INV_COUNT + 1))
    sz="$(file_size_bytes "${path}")"
    INV_BYTES=$((INV_BYTES + sz))
    if [[ ${#rel} -gt ${width} ]]; then
      width=${#rel}
    fi
  done <"${list_tmp}"

  echo "Upload inventory (${INV_COUNT} file(s), $(human_bytes "${INV_BYTES}") total):"
  if [[ "${INV_COUNT}" -eq 0 ]]; then
    echo "  (none)"
  else
    while IFS= read -r rel; do
      [[ -n "${rel}" ]] || continue
      path="${base}/${rel}"
      [[ -f "${path}" ]] || continue
      sz="$(file_size_bytes "${path}")"
      printf '  %-*s  %12s  (%s)\n' "${width}" "${rel}" "${sz}" "$(human_bytes "${sz}")"
    done <"${list_tmp}"
  fi
  echo ""
  echo "Potential upload: ${INV_COUNT} file(s), ${INV_BYTES} bytes ($(human_bytes "${INV_BYTES}"))"
  echo ""

  INV_LIST_FILE="${list_tmp}"
}

# Build "relpath -> size" map from rclone lsl (size date time path...).
# Writes "size<TAB>relpath" lines to $1.
remote_size_map() {
  local out="${1}"
  local filter="${2}"
  : >"${out}"
  # Dropbox may be empty / missing on first upload — treat as no remote files.
  rclone lsl "${REMOTE_PATH}" \
    --filter-from "${filter}" \
    2>/dev/null \
    | awk '{
        sz = $1
        # rclone lsl: SIZE DATE TIME PATH... (path may contain spaces — join $4..)
        path = $4
        for (i = 5; i <= NF; i++) path = path " " $i
        if (path != "") print sz "\t" path
      }' >"${out}" || true
}

lookup_size() {
  local mapfile="${1}"
  local key="${2}"
  awk -F'\t' -v k="${key}" '$2 == k { print $1; found=1; exit } END { if (!found) exit 1 }' "${mapfile}" 2>/dev/null || true
}

auto_detect_sync_source() {
  local best="" best_n=0 dir n
  while IFS= read -r dir; do
    [[ -d "${dir}" ]] || continue
    n="$(count_capture_files "${dir}")"
    if [[ "${n}" -gt "${best_n}" ]]; then
      best_n="${n}"
      best="${dir}"
    fi
  done < <(capture_candidate_dirs)
  if [[ "${best_n}" -gt 0 && -n "${best}" ]]; then
    echo "${best}"
    return 0
  fi
  return 1
}

resolve_source() {
  local candidate="${1:-}"
  if [[ -z "${candidate}" ]]; then
    if candidate="$(auto_detect_sync_source)"; then
      echo "Auto-detected SYNC_SOURCE: ${candidate}" >&2
      cd "${candidate}" && pwd
      return 0
    fi
    echo "Set SYNC_SOURCE in rclone.env (or pass SYNC_SOURCE=... on the command line)." >&2
    echo "Example (bench dev):  SYNC_SOURCE=\"dev\"" >&2
    echo "Example (field prod): SYNC_SOURCE=\"prod\"" >&2
    exit 1
  fi
  if [[ "${candidate}" != /* ]]; then
    candidate="${ETHERGOLD_ROOT}/${candidate#./}"
  fi
  if [[ ! -d "${candidate}" ]]; then
    echo "SYNC_SOURCE is not a directory: ${candidate}" >&2
    echo "" >&2
    echo "On this Pi, check where captures landed:" >&2
    local hint
    while IFS= read -r hint; do
      if [[ -d "${hint}" ]]; then
        echo "  found: ${hint}" >&2
        if [[ -d "${hint}/data" ]]; then
          echo "         $(find "${hint}/data" -maxdepth 1 -type f 2>/dev/null | wc -l) file(s) in data/" >&2
        fi
      fi
    done < <(capture_candidate_dirs)
    if candidate="$(auto_detect_sync_source)"; then
      echo "" >&2
      echo "Auto-fallback to: ${candidate}" >&2
      cd "${candidate}" && pwd
      return 0
    fi
    exit 1
  fi
  cd "${candidate}" && pwd
}

prod_capture_mode() {
  local base="${1}"
  [[ -f "${base}/errors.jsonl" ]] && [[ ! -f "${base}/data/events.jsonl" ]] \
    || { [[ -f "${base}/prod-data/errors.jsonl" ]] && [[ ! -f "${base}/data/events.jsonl" ]]; }
}

suggest_sync_source() {
  local best="" best_n=0 dir n
  if best="$(auto_detect_sync_source)"; then
    n="$(count_capture_files "${best}")"
    echo "Try updating rclone.env SYNC_SOURCE or run:" >&2
    if [[ "${best}" == "${ETHERGOLD_ROOT}/"* ]]; then
      echo "  SYNC_SOURCE=\"${best#${ETHERGOLD_ROOT}/}\" bash sync_to_dropbox.sh" >&2
    else
      echo "  SYNC_SOURCE=\"${best}\" bash sync_to_dropbox.sh" >&2
    fi
    return 0
  fi
  while IFS= read -r dir; do
    [[ -d "${dir}" ]] || continue
    n="$(count_capture_files "${dir}")"
    if [[ "${n}" -gt "${best_n}" ]]; then
      best_n="${n}"
      best="${dir}"
    fi
  done < <(capture_candidate_dirs)
  if [[ "${best_n}" -gt 0 && -n "${best}" ]]; then
    echo "Try updating rclone.env SYNC_SOURCE or run:" >&2
    if [[ "${best}" == "${ETHERGOLD_ROOT}/"* ]]; then
      echo "  SYNC_SOURCE=\"${best#${ETHERGOLD_ROOT}/}\" bash sync_to_dropbox.sh" >&2
    else
      echo "  SYNC_SOURCE=\"${best}\" bash sync_to_dropbox.sh" >&2
    fi
  fi
}

# Classify each inventory file vs remote size map; print + return counts via globals.
# UPLOAD_NEEDED / SKIP_UNCHANGED lists written to temp files.
classify_vs_remote() {
  local base="${1}"
  local inv_list="${2}"
  local remote_map="${3}"
  local rel path local_sz remote_sz

  NEED_LIST="$(mktemp)"
  SKIP_LIST="$(mktemp)"
  NEED_COUNT=0
  SKIP_COUNT=0
  NEED_BYTES=0

  while IFS= read -r rel; do
    [[ -n "${rel}" ]] || continue
    path="${base}/${rel}"
    [[ -f "${path}" ]] || continue
    local_sz="$(file_size_bytes "${path}")"
    remote_sz="$(lookup_size "${remote_map}" "${rel}")"
    if [[ -n "${remote_sz}" && "${remote_sz}" == "${local_sz}" ]]; then
      printf '%s\t%s\n' "${rel}" "${local_sz}" >>"${SKIP_LIST}"
      SKIP_COUNT=$((SKIP_COUNT + 1))
    else
      printf '%s\t%s\n' "${rel}" "${local_sz}" >>"${NEED_LIST}"
      NEED_COUNT=$((NEED_COUNT + 1))
      NEED_BYTES=$((NEED_BYTES + local_sz))
    fi
  done <"${inv_list}"

  echo "vs remote: ${NEED_COUNT} to upload ($(human_bytes "${NEED_BYTES}")), ${SKIP_COUNT} unchanged (will skip)"
  echo ""
}

print_upload_summary() {
  local base="${1}"
  local inv_list="${2}"
  local remote_map_after="${3}"
  local need_list="${4}"
  local skip_list="${5}"
  local rel local_sz remote_sz
  local uploaded=0 failed=0
  local fail_list

  fail_list="$(mktemp)"

  while IFS=$'\t' read -r rel local_sz; do
    [[ -n "${rel}" ]] || continue
    remote_sz="$(lookup_size "${remote_map_after}" "${rel}")"
    if [[ -n "${remote_sz}" && "${remote_sz}" == "${local_sz}" ]]; then
      uploaded=$((uploaded + 1))
    else
      printf '%s\t%s\n' "${rel}" "${local_sz}" >>"${fail_list}"
      failed=$((failed + 1))
    fi
  done <"${need_list}"

  echo "======== upload summary ========"
  echo "Candidates:     $(wc -l <"${inv_list}" | tr -d ' ') file(s)"
  echo "Uploaded/new:   ${uploaded} file(s)"
  echo "Skipped:        $(wc -l <"${skip_list}" | tr -d ' ') file(s) (already on Dropbox, same size)"
  if [[ "${failed}" -gt 0 ]]; then
    echo "Failed/missing: ${failed} file(s)"
  fi
  if [[ -s "${skip_list}" ]]; then
    echo ""
    echo "Skipped (unchanged):"
    while IFS=$'\t' read -r rel local_sz; do
      printf '  %s  (%s)\n' "${rel}" "$(human_bytes "${local_sz}")"
    done <"${skip_list}"
  fi
  if [[ "${failed}" -gt 0 ]]; then
    echo ""
    echo "Not confirmed on remote after copy:"
    while IFS=$'\t' read -r rel local_sz; do
      remote_sz="$(lookup_size "${remote_map_after}" "${rel}")"
      if [[ -z "${remote_sz}" ]]; then
        printf '  %s  (missing on remote, local %s)\n' "${rel}" "$(human_bytes "${local_sz}")"
      else
        printf '  %s  (size mismatch local=%s remote=%s)\n' \
          "${rel}" "$(human_bytes "${local_sz}")" "$(human_bytes "${remote_sz}")"
      fi
    done <"${fail_list}"
  fi
  echo "================================"
  rm -f "${fail_list}"
}

# PI_LABEL adds an extra remote subfolder. Leave empty when DROPBOX_DEST already
# identifies the fixture position (e.g. .../4-Board Test Fixture/1).
if [[ -n "${PI_LABEL:-}" ]]; then
  REMOTE_PATH="${RCLONE_REMOTE}:${DROPBOX_DEST}/${PI_LABEL}"
else
  REMOTE_PATH="${RCLONE_REMOTE}:${DROPBOX_DEST}"
fi

DRY_RUN="${DRY_RUN:-0}"
RCLONE_ARGS=()
if [[ "${DRY_RUN}" == "1" ]]; then
  RCLONE_ARGS+=(--dry-run)
  echo "DRY RUN — nothing will be uploaded."
  echo ""
fi

INV_LIST_FILE=""
REMOTE_MAP_BEFORE=""
REMOTE_MAP_AFTER=""
NEED_LIST=""
SKIP_LIST=""
cleanup_tmps() {
  rm -f "${INV_LIST_FILE}" "${REMOTE_MAP_BEFORE}" "${REMOTE_MAP_AFTER}" \
    "${NEED_LIST}" "${SKIP_LIST}" 2>/dev/null || true
}
trap cleanup_tmps EXIT

run_capture_or_prod_copy() {
  local base="${1}"
  local mode="${2}" # capture | prod
  local filter="${3}"

  print_upload_inventory "${base}" "${mode}"
  if [[ "${INV_COUNT}" -eq 0 ]]; then
    echo "ERROR: No capture files under ${base}" >&2
    if [[ "${mode}" == "capture" ]]; then
      echo "Expected data/** and/or logs/** under ${base} (all rollovers included)" >&2
      suggest_sync_source
    else
      echo "Expected capture/log files under ${base}" >&2
    fi
    exit 1
  fi

  REMOTE_MAP_BEFORE="$(mktemp)"
  REMOTE_MAP_AFTER="$(mktemp)"
  echo "Checking remote sizes at ${REMOTE_PATH} ..."
  remote_size_map "${REMOTE_MAP_BEFORE}" "${filter}"
  classify_vs_remote "${base}" "${INV_LIST_FILE}" "${REMOTE_MAP_BEFORE}"

  if [[ "${NEED_COUNT}" -eq 0 && "${DRY_RUN}" != "1" ]]; then
    echo "Nothing new to upload — all ${SKIP_COUNT} candidate(s) already match Dropbox."
    print_upload_summary "${base}" "${INV_LIST_FILE}" "${REMOTE_MAP_BEFORE}" \
      "${NEED_LIST}" "${SKIP_LIST}"
    return 0
  fi

  # Quiet upload — no -v/--progress (stop_fixture_session used to append MB of rclone noise).
  RCLONE_VERBOSE="${RCLONE_VERBOSE:-0}"
  local copy_args=("${RCLONE_ARGS[@]}")
  if [[ "${RCLONE_VERBOSE}" == "1" ]]; then
    copy_args+=(--progress -v)
  else
    copy_args+=(--stats-one-line -q)
  fi

  rclone copy "${base}" "${REMOTE_PATH}" \
    "${copy_args[@]}" \
    --filter-from "${filter}" \
    --create-empty-src-dirs

  if [[ "${DRY_RUN}" == "1" ]]; then
    echo ""
    echo "Dry run complete. Re-run without DRY_RUN=1 to upload."
    echo "(Would upload ${NEED_COUNT} file(s), skip ${SKIP_COUNT} unchanged.)"
    return 0
  fi

  echo ""
  echo "Verifying remote after copy ..."
  remote_size_map "${REMOTE_MAP_AFTER}" "${filter}"
  print_upload_summary "${base}" "${INV_LIST_FILE}" "${REMOTE_MAP_AFTER}" \
    "${NEED_LIST}" "${SKIP_LIST}"
}

PROD_SYNC="${PROD_SYNC:-0}"
if [[ "${PROD_SYNC}" == "1" ]]; then
  SYNC_SOURCE="${ETHERGOLD_ROOT}/logs"
  if [[ ! -d "${SYNC_SOURCE}" ]]; then
    echo "ERROR: production logs dir missing: ${SYNC_SOURCE}" >&2
    exit 1
  fi
  echo "Source:  ${SYNC_SOURCE}"
  echo "Remote:  ${REMOTE_PATH}"
  echo "Mode:   production (logs/errors.jsonl + supervisor.log)"
  echo ""
  run_capture_or_prod_copy "${SYNC_SOURCE}" "prod" "${ROOT}/upload_prod.rclone-filter"
  exit 0
fi

SYNC_SOURCE="$(resolve_source "${SYNC_SOURCE:-}")"
LOCAL_COUNT="$(count_capture_files "${SYNC_SOURCE}")"

# Legacy rclone.env may still point at bench tree while captures live under ~/EtherGold/bridge/.
if [[ "${LOCAL_COUNT}" -eq 0 ]]; then
  if fallback="$(auto_detect_sync_source)"; then
    fb_count="$(count_capture_files "${fallback}")"
    if [[ "${fb_count}" -gt 0 && "${fallback}" != "${SYNC_SOURCE}" ]]; then
      echo "WARN: no files under configured SYNC_SOURCE; using ${fallback}" >&2
      SYNC_SOURCE="${fallback}"
      LOCAL_COUNT="${fb_count}"
    fi
  fi
fi

echo "Source:  ${SYNC_SOURCE}"
echo "Remote:  ${REMOTE_PATH}"
if prod_capture_mode "${SYNC_SOURCE}"; then
  echo "Mode:   production (logs/errors.jsonl + supervisor.log)"
  echo ""
  run_capture_or_prod_copy "${SYNC_SOURCE}" "prod" "${ROOT}/upload_prod.rclone-filter"
else
  echo "Mode:   debug capture (data/** + logs/**, all rollovers; skip .sync_last.log only)"
  echo ""
  run_capture_or_prod_copy "${SYNC_SOURCE}" "capture" "${ROOT}/upload_capture.rclone-filter"
fi

echo ""
if [[ "${DRY_RUN}" != "1" ]]; then
  echo "Verify on PC:"
  echo "  rclone ls \"${REMOTE_PATH}\""
  echo "  or open the matching folder in Dropbox."
fi
