#!/usr/bin/env bash
# Bidirectional scp wrapper for the kobald shared volume.
# Remote paths are relative to SHARED_FOLDER_REMOTE_BASE only.
# Local paths must be absolute (any Mac volume).
#
# Usage:
#   accessSharedFolder.sh get  <remote-rel> <local-abs>
#   accessSharedFolder.sh put  <local-abs>  <remote-rel>
#   accessSharedFolder.sh ls   [remote-rel]
#
# Config (env overrides yaml defaults set by hermes-worker.js):
#   SHARED_FOLDER_REMOTE_HOST  default: root@kobald.com
#   SHARED_FOLDER_REMOTE_BASE  default: /mnt/HC_Volume_105777295/hermes-agent
#
# When Hermes runs this script via the terminal tool, the tool call MUST use
# timeout=14400 (seconds). A 60s parent timeout causes exit 124 and truncated files.
# This script has no internal transfer timeout — scp runs until complete or error.

set -euo pipefail

REMOTE_HOST="${SHARED_FOLDER_REMOTE_HOST:-root@kobald.com}"
REMOTE_BASE="${SHARED_FOLDER_REMOTE_BASE:-/mnt/HC_Volume_105777295/hermes-agent}"
REMOTE_BASE="${REMOTE_BASE%/}"

REQUIRED_TERMINAL_TIMEOUT_SEC="${SHARED_FOLDER_REQUIRED_TERMINAL_TIMEOUT:-14400}"

SSH_OPTS=(
  -o BatchMode=yes
  -o ServerAliveInterval=60
  -o ServerAliveCountMax=240
  -o TCPKeepAlive=yes
)
SCP_OPTS=(
  -o BatchMode=yes
  -o ServerAliveInterval=60
  -o ServerAliveCountMax=240
  -o TCPKeepAlive=yes
)

# After scp, poll remote/local size until it matches or retry the transfer.
VERIFY_RETRIES="${SHARED_FOLDER_VERIFY_RETRIES:-3}"
VERIFY_WAIT_SEC="${SHARED_FOLDER_VERIFY_WAIT_SEC:-2}"
VERIFY_POLLS="${SHARED_FOLDER_VERIFY_POLLS:-5}"

# Set by transfer_put on success; trap removes partial remote file on kill/failure.
_TRANSFER_OK=0
_ACTIVE_REMOTE_ABS=""
_ACTIVE_EXPECTED_BYTES=0

cleanup_on_exit() {
  local rc=$?
  if [[ "$_TRANSFER_OK" -eq 1 || -z "$_ACTIVE_REMOTE_ABS" ]]; then
    return
  fi
  local actual
  actual="$(remote_bytes "$_ACTIVE_REMOTE_ABS" 2>/dev/null || echo 0)"
  if [[ "$actual" -gt 0 && "$actual" != "$_ACTIVE_EXPECTED_BYTES" ]]; then
    echo "  removing incomplete remote file ($actual bytes, expected $_ACTIVE_EXPECTED_BYTES)..." >&2
    ssh_remote "rm -f $(printf '%q' "$_ACTIVE_REMOTE_ABS")" 2>/dev/null || true
  fi
  return
}
trap cleanup_on_exit EXIT

die() {
  echo "ERROR: $*" >&2
  exit 1
}

echo "accessSharedFolder: requires terminal timeout >= ${REQUIRED_TERMINAL_TIMEOUT_SEC}s when run via Hermes (exit 124 = parent timed out too early)"

usage() {
  cat >&2 <<EOF
Usage:
  $(basename "$0") get  <remote-rel> <local-abs>
  $(basename "$0") put  <local-abs>  <remote-rel>
  $(basename "$0") ls   [remote-rel]

Remote host: $REMOTE_HOST
Remote base: $REMOTE_BASE
EOF
  exit 1
}

resolve_local() {
  local p="$1"
  [[ "$p" == /* ]] || die "local path must be absolute: $p"
  local dir base
  dir="$(dirname "$p")"
  base="$(basename "$p")"
  [[ -d "$dir" ]] || die "cannot resolve local path (parent missing): $p"
  echo "$(cd "$dir" && pwd)/$base"
}

validate_remote_rel() {
  local rel="$1"
  [[ -n "$rel" ]] || die "remote path cannot be empty"
  [[ "$rel" != /* ]] || die "remote path must be relative (not absolute): $rel"
  [[ "$rel" != *":"* ]] || die "remote path cannot contain ':': $rel"

  local part
  IFS='/' read -r -a parts <<< "$rel"
  for part in "${parts[@]}"; do
    [[ -n "$part" ]] || die "remote path has empty segment: $rel"
    [[ "$part" != ".." ]] || die "remote path cannot contain '..': $rel"
  done
  echo "$rel"
}

remote_abs() {
  echo "${REMOTE_BASE}/${1}"
}

ssh_remote() {
  ssh "${SSH_OPTS[@]}" "$REMOTE_HOST" "$@"
}

remote_exists() {
  local rel="$1"
  ssh_remote "test -e $(printf '%q' "$(remote_abs "$rel")")"
}

remote_is_dir() {
  local rel="$1"
  ssh_remote "test -d $(printf '%q' "$(remote_abs "$rel")")"
}

check_remote_parent_exists() {
  local rel="$1"
  local parent
  parent="$(dirname "$rel")"
  if [[ "$parent" == "." ]]; then
    ssh_remote "test -d $(printf '%q' "$REMOTE_BASE")" \
      || die "remote base does not exist: $REMOTE_BASE"
  else
    remote_is_dir "$parent" || die "remote directory does not exist: $parent"
  fi
}

local_bytes() {
  local path="$1"
  if [[ -f "$path" ]]; then
    stat -f%z "$path"
  elif [[ -d "$path" ]]; then
    find "$path" -type f -print0 | xargs -0 stat -f%z 2>/dev/null | awk '{s+=$1} END {print s+0}'
  else
    echo 0
  fi
}

remote_bytes() {
  local abs="$1"
  ssh_remote "if [ -f $(printf '%q' "$abs") ]; then stat -c%s $(printf '%q' "$abs"); elif [ -d $(printf '%q' "$abs") ]; then find $(printf '%q' "$abs") -type f -print0 | xargs -0 stat -c%s 2>/dev/null | awk '{s+=\$1} END {print s+0}'; else echo 0; fi"
}

run_scp() {
  scp "${SCP_OPTS[@]}" "$@"
}

remove_stale_remote() {
  local remote_abs="$1" expected="$2"
  local actual
  if ! ssh_remote "test -e $(printf '%q' "$remote_abs")"; then
    return 0
  fi
  actual="$(remote_bytes "$remote_abs")"
  if [[ "$actual" != "$expected" ]]; then
    echo "  removing stale incomplete remote ($actual bytes, need $expected)..." >&2
    ssh_remote "rm -f $(printf '%q' "$remote_abs")"
  fi
}

transfer_put() {
  local local_src="$1" remote_abs="$2"
  local expected actual attempt poll

  _ACTIVE_REMOTE_ABS="$remote_abs"
  _TRANSFER_OK=0

  for ((attempt = 1; attempt <= VERIFY_RETRIES; attempt++)); do
    expected="$(local_bytes "$local_src")"
    _ACTIVE_EXPECTED_BYTES="$expected"
    [[ "$expected" -gt 0 ]] || die "local source is empty or unreadable: $local_src"
    echo "  expected size: $expected bytes (attempt $attempt/$VERIFY_RETRIES)"

    remove_stale_remote "$remote_abs" "$expected"

    if [[ -d "$local_src" ]]; then
      run_scp -r "$local_src" "${REMOTE_HOST}:${remote_abs}"
    else
      run_scp "$local_src" "${REMOTE_HOST}:${remote_abs}"
    fi

    for ((poll = 1; poll <= VERIFY_POLLS; poll++)); do
      actual="$(remote_bytes "$remote_abs")"
      if [[ "$actual" == "$expected" ]]; then
        echo "  verified remote size: $actual bytes"
        _TRANSFER_OK=1
        _ACTIVE_REMOTE_ABS=""
        return 0
      fi
      if [[ "$poll" -lt "$VERIFY_POLLS" ]]; then
        echo "  remote size $actual != $expected bytes — waiting ${VERIFY_WAIT_SEC}s (${poll}/${VERIFY_POLLS})..." >&2
        sleep "$VERIFY_WAIT_SEC"
      fi
    done

    echo "  WARN: size mismatch after scp (expected $expected, got $actual) — retrying transfer..." >&2
    remove_stale_remote "$remote_abs" "$expected"
  done

  die "put incomplete after $VERIFY_RETRIES attempts: expected $expected bytes, remote has $actual bytes"
}

transfer_get() {
  local remote_abs="$1" local_dest="$2" remote_rel="$3"
  local expected actual attempt poll
  local is_dir=0
  if ssh_remote "test -d $(printf '%q' "$remote_abs")"; then
    is_dir=1
  fi

  for ((attempt = 1; attempt <= VERIFY_RETRIES; attempt++)); do
    expected="$(remote_bytes "$remote_abs")"
    [[ "$expected" -gt 0 ]] || die "remote source is empty or unreadable: $remote_rel"
    echo "  expected size: $expected bytes (attempt $attempt/$VERIFY_RETRIES)"

    if [[ "$is_dir" -eq 1 ]]; then
      run_scp -r "${REMOTE_HOST}:${remote_abs}" "$local_dest"
      # scp -r creates basename under local_dest or uses local_dest as target dir
      if [[ -d "$local_dest" ]]; then
        actual="$(local_bytes "$local_dest")"
      else
        actual="$(local_bytes "$(dirname "$local_dest")/$(basename "$remote_abs")")"
      fi
    else
      run_scp "${REMOTE_HOST}:${remote_abs}" "$local_dest"
      actual="$(local_bytes "$local_dest")"
    fi

    for ((poll = 1; poll <= VERIFY_POLLS; poll++)); do
      if [[ "$actual" == "$expected" ]]; then
        echo "  verified local size: $actual bytes"
        return 0
      fi
      if [[ "$poll" -lt "$VERIFY_POLLS" ]]; then
        echo "  local size $actual != $expected bytes — waiting ${VERIFY_WAIT_SEC}s (${poll}/${VERIFY_POLLS})..." >&2
        sleep "$VERIFY_WAIT_SEC"
        if [[ "$is_dir" -eq 1 && -d "$local_dest" ]]; then
          actual="$(local_bytes "$local_dest")"
        else
          actual="$(local_bytes "$local_dest")"
        fi
      fi
    done

    echo "  WARN: size mismatch after scp (expected $expected, got $actual) — retrying transfer..." >&2
  done

  die "get incomplete after $VERIFY_RETRIES attempts: expected $expected bytes, local has $actual bytes"
}

cmd_ls() {
  local rel="${1:-.}"
  if [[ "$rel" == "." ]]; then
    echo "LIST remote:$REMOTE_HOST:$REMOTE_BASE"
    ssh_remote "ls -la $(printf '%q' "$REMOTE_BASE")"
    return
  fi
  rel="$(validate_remote_rel "$rel")"
  remote_exists "$rel" || die "remote path not found: $rel"
  echo "LIST remote:$REMOTE_HOST:$(remote_abs "$rel")"
  ssh_remote "ls -la $(printf '%q' "$(remote_abs "$rel")")"
}

cmd_get() {
  local rel="$1" local_dest="$2"
  rel="$(validate_remote_rel "$rel")"
  local_dest="$(resolve_local "$local_dest")"

  remote_exists "$rel" || die "remote path does not exist: $rel"

  local local_parent
  local_parent="$(dirname "$local_dest")"
  [[ -d "$local_parent" ]] || die "local directory does not exist: $local_parent"

  local remote
  remote="$(remote_abs "$rel")"
  echo "GET remote:$REMOTE_HOST:$remote -> local:$local_dest"
  transfer_get "$remote" "$local_dest" "$rel"
}

cmd_put() {
  local local_src="$1" rel="$2"
  local_src="$(resolve_local "$local_src")"
  [[ -e "$local_src" ]] || die "local path does not exist: $local_src"
  rel="$(validate_remote_rel "$rel")"
  check_remote_parent_exists "$rel"

  local remote
  remote="$(remote_abs "$rel")"
  echo "PUT local:$local_src -> remote:$REMOTE_HOST:$remote"
  transfer_put "$local_src" "$remote"
}

[[ $# -ge 1 ]] || usage

case "$1" in
  get)
    [[ $# -eq 3 ]] || usage
    cmd_get "$2" "$3"
    ;;
  put)
    [[ $# -eq 3 ]] || usage
    cmd_put "$2" "$3"
    ;;
  ls)
    [[ $# -le 2 ]] || usage
    cmd_ls "${2:-.}"
    ;;
  -h|--help|help)
    usage
    ;;
  *)
    die "unknown command: $1 (expected get, put, or ls)"
    ;;
esac
