#!/usr/bin/bash
set -uo pipefail

CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/generalshare"
CONFIG="$CONFIG_DIR/config.json"

GUI=0
HUB_OVERRIDE=""
CODE_OVERRIDE=""
PAIR_ONLY=0
FILES=()
TMP=""

usage() {
  cat <<'EOF'
GeneralShare Linux sender

Usage:
  generalshare-send.sh [--gui] [--hub URL] [--code 123456] <file-or-directory>...
  generalshare-send.sh --hub URL --code 123456 --pair

Options:
  --gui        Use zenity dialogs (for file-manager scripts)
  --hub URL    Use a specific hub, e.g. http://192.168.1.20:5134
  --code CODE  Pairing code (otherwise prompted)
  --pair       Pair only and save the token, do not send
EOF
}

while [ $# -gt 0 ]; do
  case "$1" in
    --gui) GUI=1; shift ;;
    --hub) HUB_OVERRIDE="${2:-}"; shift 2 ;;
    --code) CODE_OVERRIDE="${2:-}"; shift 2 ;;
    --pair) PAIR_ONLY=1; shift ;;
    --help|-h) usage; exit 0 ;;
    --) shift; FILES+=("$@"); break ;;
    -*) echo "Unknown option: $1" >&2; usage; exit 2 ;;
    *) FILES+=("$1"); shift ;;
  esac
done

is_gui() {
  [ "$GUI" -eq 1 ] && command -v zenity >/dev/null 2>&1
}

say() {
  if ! is_gui; then
    echo "$*"
  fi
}

notify() {
  if command -v notify-send >/dev/null 2>&1; then
    notify-send "GeneralShare" "$*" >/dev/null 2>&1 || true
  fi
}

fail() {
  local message="$*"
  echo "GeneralShare: $message" >&2
  if is_gui; then
    zenity --error --title="GeneralShare" --text="$message" >/dev/null 2>&1 || true
  fi
  exit 1
}

prompt_code() {
  if [ -n "$CODE_OVERRIDE" ]; then
    printf '%s' "$CODE_OVERRIDE"
    return
  fi
  if is_gui; then
    zenity --entry \
      --title="GeneralShare" \
      --text="Enter the 6-digit code shown on the receiver:" \
      --entry-text=""
  else
    read -r -p "Pairing code: " code
    printf '%s' "$code"
  fi
}

load_saved_hub() {
  [ -f "$CONFIG" ] || return 0
  jq -r '.hub // empty' "$CONFIG" 2>/dev/null || true
}

load_token() {
  [ -f "$CONFIG" ] || return 0
  jq -r '.token // empty' "$CONFIG" 2>/dev/null || true
}

save_config() {
  mkdir -p "$CONFIG_DIR"
  jq -n \
    --arg hub "$1" \
    --arg token "$2" \
    --arg name "$3" \
    --arg id "$4" \
    '{hub:$hub,token:$token,name:$name,id:$id}' > "$CONFIG"
  chmod 600 "$CONFIG" 2>/dev/null || true
}

load_session() {
  [ -f "$CONFIG" ] || return 0
  jq -r '.session // empty' "$CONFIG" 2>/dev/null || true
}

load_signature() {
  [ -f "$CONFIG" ] || return 0
  jq -r '.signature // empty' "$CONFIG" 2>/dev/null || true
}

save_session_state() {
  local session="$1" signature="$2" tmp="$CONFIG.tmp"
  jq --arg session "$session" --arg signature "$signature" \
    '.session=$session | .signature=$signature' "$CONFIG" > "$tmp" &&
    mv "$tmp" "$CONFIG"
  chmod 600 "$CONFIG" 2>/dev/null || true
}

clear_session_state() {
  [ -f "$CONFIG" ] || return 0
  local tmp="$CONFIG.tmp"
  jq 'del(.session,.signature)' "$CONFIG" > "$tmp" && mv "$tmp" "$CONFIG"
  chmod 600 "$CONFIG" 2>/dev/null || true
}

discover_hubs() {
  command -v avahi-browse >/dev/null 2>&1 || return 0
  avahi-browse -rt _generalshare._tcp 2>/dev/null | python3 -c '
import sys, re, json
records = []
cur = {}
def flush():
    global cur
    if cur.get("name") and cur.get("port"):
        ip = cur.get("ip") or cur.get("address")
        if ip and not ip.startswith("127.") and not ip.startswith("fe80"):
            records.append({
                "name": cur.get("name"),
                "id": cur.get("id") or cur.get("name"),
                "short": cur.get("short", ""),
                "platform": cur.get("platform", "unknown"),
                "ip": ip,
                "port": int(cur["port"]),
            })
    cur = {}
for line in sys.stdin:
    if line.startswith("="):
        flush()
    elif "address = [" in line:
        m = re.search(r"address = \[([^\]]+)\]", line)
        if m: cur["address"] = m.group(1)
    elif "port = [" in line:
        m = re.search(r"port = \[([^\]]+)\]", line)
        if m: cur["port"] = m.group(1)
    elif "txt = [" in line:
        for item in re.findall(r"\"([^\"]*)\"", line):
            if "=" in item:
                key, value = item.split("=", 1)
                cur[key] = value
flush()
seen = set()
for record in records:
    key = record["id"] + "@" + record["ip"] + ":" + str(record["port"])
    if key in seen:
        continue
    seen.add(key)
    print(json.dumps(record, ensure_ascii=False))
'
}

select_hub() {
  if [ -n "$HUB_OVERRIDE" ]; then
    HUB="$HUB_OVERRIDE"
    return
  fi
  local saved
  saved=$(load_saved_hub)
  if [ -n "$saved" ]; then
    HUB="$saved"
    return
  fi

  local list
  list=$(discover_hubs)
  [ -n "$list" ] || fail "No GeneralShare receiver found. Start the receiver or pass --hub."

  local count
  count=$(printf '%s\n' "$list" | jq -s 'length')
  if [ "$count" -eq 1 ]; then
    HUB="http://$(printf '%s\n' "$list" | jq -r '.ip + ":" + (.port|tostring)')"
    return
  fi

  if is_gui; then
    local choice
    choice=$(printf '%s\n' "$list" | jq -r '[.name, (.ip + ":" + (.port|tostring)), .id] | @tsv' |
      zenity --list \
        --title="GeneralShare" \
        --text="Choose a receiver" \
        --column="Device" \
        --column="Address" \
        --column="ID" \
        --height=340 \
        --width=560)
    [ -n "${choice:-}" ] || exit 0
    HUB="http://$(printf '%s' "$choice" | awk -F'\t' '{print $2}')"
    return
  fi

  echo "Nearby GeneralShare receivers:"
  local index=0 line
  while IFS= read -r line; do
    index=$((index + 1))
    printf '  %d) %s (%s:%s)\n' \
      "$index" \
      "$(jq -r '.name' <<<"$line")" \
      "$(jq -r '.ip' <<<"$line")" \
      "$(jq -r '.port' <<<"$line")"
  done <<<"$list"
  printf 'Choose [1-%d]: ' "$count"
  read -r choice
  line=$(printf '%s\n' "$list" | sed -n "${choice}p")
  [ -n "$line" ] || fail "Invalid choice."
  HUB="http://$(jq -r '.ip + ":" + (.port|tostring)' <<<"$line")"
}

pair_hub() {
  local hub="$1" code="$2" body raw http response token name id
  body=$(jq -n \
    --arg code "$code" \
    --arg name "$(hostname)" \
    --arg platform "linux" \
    '{code:$code,name:$name,platform:$platform}')
  raw=$(curl -sS -w '\n%{http_code}' -X POST \
    -H 'Content-Type: application/json' \
    --data-binary "$body" \
    "$hub/api/pair") || fail "Cannot reach $hub"
  http=$(printf '%s\n' "$raw" | tail -n 1)
  response=$(printf '%s\n' "$raw" | sed '$d')
  [ "$http" = "200" ] || fail "Pairing failed (HTTP $http): $response"

  token=$(jq -r '.token // empty' <<<"$response")
  [ -n "$token" ] || fail "Pairing failed: $response"
  name=$(jq -r '.name // "receiver"' <<<"$response")
  id=$(jq -r '.deviceId // ""' <<<"$response")
  save_config "$hub" "$token" "$name" "$id"
  TOKEN="$token"
  notify "Paired with $name"
  say "Paired with $name"
}

ensure_token() {
  local saved_hub
  saved_hub=$(load_saved_hub)
  TOKEN=$(load_token)
  if [ "$saved_hub" != "$HUB" ]; then
    TOKEN=""
  fi
  if [ -z "$TOKEN" ]; then
    local code
    code=$(prompt_code)
    [[ "$code" =~ ^[0-9]{6}$ ]] || fail "A 6-digit pairing code is required."
    pair_hub "$HUB" "$code"
  fi
}

declare -A USED=()
UNIQUE_RESULT=""
MANIFEST_JSON=""
MANIFEST_TSV=""

unique_path() {
  local rel="$1"
  local directory="" name="$rel" stem suffix n=2 candidate
  if [ -z "${USED[$rel]:-}" ]; then
    USED[$rel]=1
    UNIQUE_RESULT="$rel"
    return
  fi
  if [[ "$rel" == */* ]]; then
    directory="${rel%/*}/"
    name="${rel##*/}"
  fi
  stem="$name"
  suffix=""
  if [[ "$name" == *.* && "${name%%.*}" != "" ]]; then
    stem="${name%.*}"
    suffix=".${name##*.}"
  fi
  candidate="$directory$stem ($n)$suffix"
  while [ -n "${USED[$candidate]:-}" ]; do
    n=$((n + 1))
    candidate="$directory$stem ($n)$suffix"
  done
  USED[$candidate]=1
  UNIQUE_RESULT="$candidate"
}

add_manifest_item() {
  local json="$1" tsv="$2" file="$3" rel="$4" first="$5"
  local size hash item
  size=$(stat -c %s "$file") || fail "Cannot read $file"
  hash=$(sha256sum "$file" | awk '{print $1}')
  item=$(jq -n \
    --arg name "$(basename "$file")" \
    --arg path "$rel" \
    --argjson size "$size" \
    --arg sha256 "$hash" \
    '{name:$name,path:$path,size:$size,sha256:$sha256}')
  if [ "$first" -eq 1 ]; then
    printf '%s' "$item" >> "$json"
  else
    printf ',%s' "$item" >> "$json"
  fi
  printf '%s\t%s\t%s\t%s\n' "$rel" "$size" "$file" "$hash" >> "$tsv"
}

build_manifest() {
  local json="$TMP/manifest.json" tsv="$TMP/manifest.tsv" first=1 path root file rel
  printf '[' > "$json"
  : > "$tsv"
  USED=()

  for path in "${FILES[@]}"; do
    [ -e "$path" ] || fail "Not found: $path"
    if [ -d "$path" ]; then
      root=$(basename "$path")
      while IFS= read -r -d '' file; do
        rel="$root/${file#"$path"/}"
        unique_path "$rel"
        rel="$UNIQUE_RESULT"
        add_manifest_item "$json" "$tsv" "$file" "$rel" "$first"
        first=0
      done < <(find "$path" -type f ! -name '.DS_Store' -print0 | sort -z)
    else
      unique_path "$(basename "$path")"
      rel="$UNIQUE_RESULT"
      add_manifest_item "$json" "$tsv" "$path" "$rel" "$first"
      first=0
    fi
  done

  printf ']' >> "$json"
  [ "$first" -eq 0 ] || fail "No files to send."
  MANIFEST_JSON="$json"
  MANIFEST_TSV="$tsv"
}

wait_accept() {
  local session_id="$1" receiver="$2" status="pending" deadline=$((SECONDS + 180)) zpid=""
  if is_gui; then
    zenity --progress \
      --pulsate \
      --title="GeneralShare" \
      --text="Waiting for $receiver to accept…" \
      --no-cancel >/dev/null 2>&1 &
    zpid=$!
  else
    say "Waiting for $receiver to accept…"
  fi

  while [ "$SECONDS" -lt "$deadline" ]; do
    status=$(curl -sS "$HUB/api/sessions/$session_id" | jq -r '.status // empty' 2>/dev/null)
    case "$status" in
      accepted|uploading|completed) break ;;
      declined)
        [ -n "$zpid" ] && kill "$zpid" 2>/dev/null
        fail "$receiver declined the transfer."
        ;;
    esac
    sleep 1
  done
  [ -n "$zpid" ] && kill "$zpid" 2>/dev/null
  case "$status" in
    accepted|uploading|completed) ;;
    *) fail "Timed out waiting for $receiver." ;;
  esac
}

upload_files() {
  local session_id="$1" receiver="$2" rel size file index=0 total status complete offset url_path http
  total=$(wc -l < "$MANIFEST_TSV")
  while IFS=$'\t' read -r rel size file hash; do
    index=$((index + 1))
    url_path=$(jq -rn --arg value "$rel" '$value|@uri')
    status=$(curl -sS "$HUB/api/upload/status?path=$url_path&sessionId=$session_id")
    complete=$(jq -r '.complete // false' <<<"$status")
    if [ "$complete" = "true" ]; then
      continue
    fi
    offset=$(jq -r '.offset // 0' <<<"$status")
    [ "$offset" -lt "$size" ] || fail "Upload stalled for $rel."
    say "Sending $index/$total: $rel"

    if [ "$size" -eq 0 ]; then
      http=$(curl -sS -o "$TMP/upload.json" -w '%{http_code}' -X POST \
        -H "Authorization: Bearer $TOKEN" \
        -H 'Content-Type: application/octet-stream' \
        --data-binary '' \
        "$HUB/api/upload?path=$url_path&sessionId=$session_id")
    elif [ "$offset" -gt 0 ]; then
      http=$(tail -c +$((offset + 1)) "$file" | curl -sS -o "$TMP/upload.json" -w '%{http_code}' -X POST \
        -H "Authorization: Bearer $TOKEN" \
        -H 'Content-Type: application/octet-stream' \
        -H "Content-Range: bytes $offset-$((size - 1))/$size" \
        --data-binary @- \
        "$HUB/api/upload?path=$url_path&sessionId=$session_id")
    else
      http=$(curl -sS -o "$TMP/upload.json" -w '%{http_code}' -X POST \
        -H "Authorization: Bearer $TOKEN" \
        -H 'Content-Type: application/octet-stream' \
        -H "Content-Range: bytes 0-$((size - 1))/$size" \
        --data-binary @"$file" \
        "$HUB/api/upload?path=$url_path&sessionId=$session_id")
    fi

    case "$http" in
      200|202) ;;
      *) fail "Upload failed for $rel (HTTP $http): $(cat "$TMP/upload.json")" ;;
    esac
  done < "$MANIFEST_TSV"

  notify "Sent to $receiver"
  if is_gui; then
    zenity --info --title="GeneralShare" --text="Sent to $receiver." >/dev/null 2>&1 || true
  else
    say "Sent to $receiver."
  fi
}

send_files() {
  select_hub
  ensure_token
  build_manifest

  local prepare_body="$TMP/prepare-body.json" response_file="$TMP/prepare.json"
  local http session_id="" receiver="receiver" signature saved_session saved_signature saved_status
  signature=$(cut -f1,2,4 "$MANIFEST_TSV" | LC_ALL=C sort | sha256sum | awk '{print $1}')

  saved_session=$(load_session)
  saved_signature=$(load_signature)
  if [ -n "$saved_session" ] && [ "$saved_signature" = "$signature" ]; then
    saved_status=$(curl -sS "$HUB/api/sessions/$saved_session" 2>/dev/null |
      jq -r '.status // empty' 2>/dev/null || true)
    case "$saved_status" in
      pending|accepted|uploading)
        session_id="$saved_session"
        receiver=$(curl -sS "$HUB/api/sessions/$saved_session" 2>/dev/null |
          jq -r '.receiver // "receiver"' 2>/dev/null || echo receiver)
        say "Resuming session $session_id"
        ;;
      *)
        clear_session_state
        ;;
    esac
  fi

  if [ -z "$session_id" ]; then
    jq -n --arg sender "$(hostname)" --slurpfile files "$MANIFEST_JSON" \
      '{sender:$sender,files:$files[0]}' > "$prepare_body"

    http=$(curl -sS -o "$response_file" -w '%{http_code}' -X POST \
      -H "Authorization: Bearer $TOKEN" \
      -H 'Content-Type: application/json' \
      --data-binary @"$prepare_body" \
      "$HUB/api/prepare")

    if [ "$http" = "401" ]; then
      rm -f "$CONFIG"
      TOKEN=""
      ensure_token
      http=$(curl -sS -o "$response_file" -w '%{http_code}' -X POST \
        -H "Authorization: Bearer $TOKEN" \
        -H 'Content-Type: application/json' \
        --data-binary @"$prepare_body" \
        "$HUB/api/prepare")
    fi

    [ "$http" = "202" ] || fail "Prepare failed (HTTP $http): $(cat "$response_file")"
    session_id=$(jq -r '.sessionId' "$response_file")
    receiver=$(jq -r '.receiver // "receiver"' "$response_file")
    save_session_state "$session_id" "$signature"
  fi

  wait_accept "$session_id" "$receiver"
  upload_files "$session_id" "$receiver"
  clear_session_state
}

if [ "$PAIR_ONLY" -eq 1 ]; then
  [ -n "$HUB_OVERRIDE" ] || fail "--pair requires --hub."
  code=$(prompt_code)
  [[ "$code" =~ ^[0-9]{6}$ ]] || fail "A 6-digit pairing code is required."
  pair_hub "$HUB_OVERRIDE" "$code"
  exit 0
fi

if [ "${#FILES[@]}" -eq 0 ]; then
  usage
  exit 2
fi

TMP=$(mktemp -d) || fail "Cannot create a temporary directory."
trap 'rm -rf "$TMP"' EXIT
send_files
