#!/usr/bin/env bash
# 3ling-menu — Interactive menu for 3ling CLI (non-docker)

set -euo pipefail

C_RESET=$'\e[0m'
C_CYAN=$'\e[36m'
C_GREEN=$'\e[32m'
C_YELLOW=$'\e[33m'
C_RED=$'\e[31m'
C_DIM=$'\e[2m'

BORDER="━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

# optional domain file (untuk subscription link)
DOMAIN_FILE=/etc/x-ui/domain
API_HOST_FILE=/etc/x-ui/api_host
API_PORT_FILE=/etc/x-ui/api_port

read_file(){ local p="$1" def="${2:-}"; [[ -r "$p" ]] && tr -d '\r' <"$p" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' || printf %s "$def"; }

pause() { read -rp $'\nPress Enter to continue…'; }

# ---- Telegram NOTIFY (beda dari bot backup) ---------------------------------
NOTIFY_CONF=/usr/local/sbin/3ling-notify.conf

load_notify_conf() {
  [[ -f "$NOTIFY_CONF" ]] && source "$NOTIFY_CONF"
  : "${TG_NOTIFY_TOKEN:=}"   # boleh override via environment
  : "${TG_NOTIFY_CHAT_ID:=}"
}

tg_notify_enabled() {
  load_notify_conf
  [[ -n "${TG_NOTIFY_TOKEN:-}" && -n "${TG_NOTIFY_CHAT_ID:-}" ]]
}

_html_escape() {
  # escape untuk parse_mode=HTML
  sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'
}

tg_send_text() { # text -> kirim sebagai <pre> HTML
  local text="${1:-}"
  tg_notify_enabled || return 0
  curl -sS -X POST "https://api.telegram.org/bot${TG_NOTIFY_TOKEN}/sendMessage" \
       -d "chat_id=${TG_NOTIFY_CHAT_ID}" \
       --data-urlencode "parse_mode=HTML" \
       --data-urlencode "text=$(printf "<pre>%s</pre>" "$text" | _html_escape)" \
       >/dev/null || true
}

# --- Pretty JSON to Telegram (pakai NOTIFY config yang sama) -----------------
_len_bytes() { wc -c | awk '{print $1}'; }

tg_send_json_pretty() {  # tg_send_json_pretty "Judul" "$raw_json_or_text"
  tg_notify_enabled || return 0
  local title="${1:-}" raw="${2:-}" pretty msg

  # Coba pretty-print + sort key; kalau bukan JSON valid, kirim apa adanya
  if pretty="$(jq -S . <<<"$raw" 2>/dev/null)"; then
    :
  else
    pretty="$raw"
  fi

  # Kalau masih pendek, kirim inline sebagai <pre>
  if [[ $(printf "%s" "$pretty" | _len_bytes) -le 3500 ]]; then
    msg="<b>$(_html_escape <<<"$title")</b>
<pre>$(_html_escape <<<"$pretty")</pre>"
    curl -sS -X POST "https://api.telegram.org/bot${TG_NOTIFY_TOKEN}/sendMessage" \
         -d "chat_id=${TG_NOTIFY_CHAT_ID}" \
         --data-urlencode "parse_mode=HTML" \
         --data-urlencode "text=${msg}" >/dev/null || true
    return 0
  fi

  # Panjang: kirim sebagai dokumen .json
  local tmp="/tmp/3ling-$(date +%s)-out.json"
  printf "%s\n" "$pretty" > "$tmp"
  curl -sS -F "chat_id=${TG_NOTIFY_CHAT_ID}" \
       -F "document=@${tmp};type=application/json;filename=output.json" \
       -F "caption=${title}" \
       "https://api.telegram.org/bot${TG_NOTIFY_TOKEN}/sendDocument" >/dev/null || true
  rm -f "$tmp"
}

tg_send_title_body() { # "Judul" "Body multiline"
  local title="${1:-}" body="${2:-}"
  tg_notify_enabled || return 0
  local msg
  msg="<b>$title</b>
<pre>$(printf "%s" "$body" | _html_escape)</pre>"
  curl -sS -X POST "https://api.telegram.org/bot${TG_NOTIFY_TOKEN}/sendMessage" \
       -d "chat_id=${TG_NOTIFY_CHAT_ID}" \
       --data-urlencode "parse_mode=HTML" \
       --data-urlencode "text=$msg" \
       >/dev/null || true
}

header() {
  clear
  printf "%s\n" "$BORDER"
  printf "            ${C_CYAN}⇱ 3ling Interactive Menu ⇲${C_RESET}\n"
  printf "%s\n" "$BORDER"
}

run_cmd() {
  echo
  echo "${C_DIM}>> $*${C_RESET}"
  echo "------------------------------------------------------------"
  if [[ -t 1 ]]; then
    "$@" | less -S +G -K || true
  else
    "$@"
  fi
}

prompt() {
  local label="$1"; local def="${2:-}"; local ans
  if [[ -n "$def" ]]; then
    read -rp "$label [$def]: " ans
    echo "${ans:-$def}"
  else
    read -rp "$label: " ans
    echo "$ans"
  fi
}
# ---- Helpers: ambil daftar email & picker proto/transport --------------------
pick_email_from_list() {
  # Ambil email dari kolom EMAIL pada '3ling list' (kolom dipisah oleh 2+ spasi/tab)
  local raw emails=()
  raw="$(3ling list 2>/dev/null || true)"

  # Ambil field ke-4 (EMAIL) hanya pada baris yang kolom-1 numerik (INB ID)
  mapfile -t emails < <(printf "%s\n" "$raw" \
    | awk -F'[[:space:]][[:space:]]+' '($1 ~ /^[0-9]+$/) {print $4}' \
    | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
    | awk 'length>0' \
    | sort -u)

  if (( ${#emails[@]} == 0 )); then
    # fallback manual
    read -rp "Masukkan email user: " _em
    printf "%s" "$_em"
    return
  fi

  # Tambahkan opsi input manual
  emails+=("[Ketik manual…]")

  local sel
  sel="$(choose_menu "Pilih User (Email):" "${emails[@]}")"
  if [[ "$sel" == "[Ketik manual…]" ]]; then
    read -rp "Masukkan email user: " _em
    printf "%s" "$_em"
  else
    printf "%s" "$sel"
  fi
}
pick_proto_transport() {
  # Kembalikan "proto transport" lewat STDOUT (dipisah spasi)
  local proto transports transport_pick transport

  proto="$(choose_menu "Pilih Protocol:" "vless" "vmess" "trojan" "shadowsocks")"

  case "$proto" in
    vless)           transports=("tcp (reality)" "ws" "httpupgrade" "grpc" "xhttp") ;;
    vmess)           transports=("ws" "httpupgrade" "grpc" "xhttp") ;;
    trojan)          transports=("tcp" "ws" "httpupgrade" "grpc" "xhttp") ;;
    shadowsocks) transports=("ws" "httpupgrade") ;;
  esac

  transport_pick="$(choose_menu "Pilih Transport:" "${transports[@]}")"
  case "$transport_pick" in
    "tcp (reality)") transport="tcp" ;;
    *)               transport="$transport_pick" ;;
  esac

  printf "%s %s" "$proto" "$transport"
}
choose_menu() {
  # choose_menu "Title" arr_items... -> prints ONLY the selected value to STDOUT
  local title="$1"; shift
  local items=("$@")

  # Tampilkan menu ke STDERR, bukan STDOUT
  echo >&2
  echo "$title" >&2
  local i=0
  for it in "${items[@]}"; do
    i=$((i+1))
    printf "%d) %s\n" "$i" "$it" >&2
  done

  # Baca pilihan; kembalikan hanya item terpilih ke STDOUT
  local pick
  while :; do
    read -rp "Pilih [1-$i]: " pick
    if [[ "$pick" =~ ^[0-9]+$ ]] && (( pick>=1 && pick<=i )); then
      printf "%s" "${items[$((pick-1))]}"
      return 0
    fi
    echo "${C_RED}Pilihan tidak valid.${C_RESET}" >&2
  done
}

# ---- Pretty success banner after "add" (robust & clean) ----------------------
# args: proto transport email show_text days [add_json]
# ---- URI Builder -------------------------------------------------------------
_b64() { base64 -w0 2>/dev/null || base64; }

# build VLESS URIs
_uri_vless() { # email domain port proto transport uuid path sname flow sid pkey
  local email="$1" domain="$2" port="$3" proto="$4" net="$5" uuid="$6" path="$7" sname="$8" flow="$9" sid="${10}" pkey="${11}"
  local q="type=${net}"
  case "$net" in
    ws|xhttp) [[ -n "$path" ]] && q="${q}&path=$(printf %s "$path" | sed 's#^/*#/#')" ;;
    httpupgrade) [[ -n "$path" ]] && q="${q}&path=$(printf %s "$path" | sed 's#^/*#/#')" ;;
    grpc) [[ -n "$sname" ]] && q="${q}&serviceName=${sname}" ;;
    tcp)
      # reality
      q="${q}&security=reality"
      [[ -n "$pkey" ]] && q="${q}&pbk=${pkey}"
      [[ -n "$sid"  ]] && q="${q}&sid=${sid}"
      [[ -n "$flow" ]] && q="${q}&flow=${flow}"
      echo "vless://${uuid}@${domain}:${port}?${q}#${email}"
      return
      ;;
  esac
  # tls / none is decided by caller; we just pass query through
  echo "vless://${uuid}@${domain}:${port}?${q}#${email}"
}

# build VMESS URI (base64 json)
_uri_vmess() { # email domain port transport uuid path sname tls_flag
  local email="$1" domain="$2" port="$3" net="$4" uuid="$5" path="$6" sname="$7" tls="$8"
  local tls_str=""; [[ "$tls" == "1" ]] && tls_str="tls"
  local type="none"
  local add_host=""
  local j
  case "$net" in
    grpc)
      j="$(jq -nc \
        --arg v "2" \
        --arg ps "$email" \
        --arg add "$domain" \
        --arg port "$port" \
        --arg id "$uuid" \
        --arg aid "0" \
        --arg net "grpc" \
        --arg type "none" \
        --arg host "" \
        --arg path "" \
        --arg sni "$domain" \
        --arg tls "$tls_str" \
        --arg sc "$sname" \
        '{v:$v,ps:$ps,add:$add,port:$port,id:$id,aid:$aid,net:$net,type:$type,host:$host,path:$path,tls:$tls,sni:$sni,alpn:"",servicename:$sc}')"
      ;;
    httpupgrade|xhttp|ws)
      # ensure path starts with /
      [[ -n "$path" && "$path" != /* ]] && path="/$path"
      j="$(jq -nc \
        --arg v "2" \
        --arg ps "$email" \
        --arg add "$domain" \
        --arg port "$port" \
        --arg id "$uuid" \
        --arg aid "0" \
        --arg net "$net" \
        --arg type "none" \
        --arg host "" \
        --arg path "$path" \
        --arg sni "$domain" \
        --arg tls "$tls_str" \
        '{v:$v,ps:$ps,add:$add,port:$port,id:$id,aid:$aid,net:$net,type:$type,host:$host,path:$path,tls:$tls,sni:$sni,alpn:""}')"
      ;;
    *)
      # tcp (non-reality)
      j="$(jq -nc \
        --arg v "2" --arg ps "$email" --arg add "$domain" --arg port "$port" \
        --arg id "$uuid" --arg aid "0" --arg net "tcp" --arg type "none" \
        --arg host "" --arg path "" --arg sni "$domain" --arg tls "$tls_str" \
        '{v:$v,ps:$ps,add:$add,port:$port,id:$id,aid:$aid,net:$net,type:$type,host:$host,path:$path,tls:$tls,sni:$sni,alpn:""}')"
      ;;
  esac
  printf "vmess://%s" "$(_b64 <<<"$j")"
}

# build TROJAN URIs
_uri_trojan() { # email domain port transport password path sname tls_flag
  local email="$1" domain="$2" port="$3" net="$4" pass="$5" path="$6" sname="$7" tls="$8"
  local q="type=${net}"
  case "$net" in
    ws|xhttp|httpupgrade)
      [[ -n "$path" ]] && q="${q}&path=$(printf %s "$path" | sed 's#^/*#/#')"
      ;;
    grpc)
      [[ -n "$sname" ]] && q="${q}&serviceName=${sname}"
      ;;
    tcp)
      q="${q}"
      ;;
  esac
  [[ "$tls" == "1" ]] && q="${q}&security=tls"
  echo "trojan://${pass}@${domain}:${port}?${q}#${email}"
}

# wrapper: print TLS and (if relevant) nTLS
print_uris() { # proto transport email domain uuid pass path sname flow sid pkey
  local proto="$1" net="$2" email="$3" domain="$4" uuid="$5" pass="$6" path="$7" sname="$8" flow="$9" sid="${10}" pkey="${11}"
  local tls_uri="" ntls_uri=""

  case "$proto" in
    vless)
      if [[ "$net" == "tcp" ]]; then
        tls_uri="$(_uri_vless "$email" "$domain" "443" "$proto" "$net" "$uuid" "$path" "$sname" "$flow" "$sid" "$pkey")"
      else
        tls_uri="$(_uri_vless "$email" "$domain" "443" "$proto" "$net" "$uuid" "$path" "$sname" "" "" "")&security=tls"
        ntls_uri="$(_uri_vless "$email" "$domain" "80"  "$proto" "$net" "$uuid" "$path" "$sname" "" "" "")"
      fi
      ;;
    vmess)
      if [[ "$net" == "grpc" ]]; then
        tls_uri="$(_uri_vmess "$email" "$domain" "443" "$net" "$uuid" "$path" "$sname" "1")"
      elif [[ "$net" == "tcp" ]]; then
        # tcp (non-reality) – give TLS and nTLS variants
        tls_uri="$(_uri_vmess "$email" "$domain" "443" "$net" "$uuid" "" "" "1")"
        ntls_uri="$(_uri_vmess "$email" "$domain" "80"  "$net" "$uuid" "" "" "0")"
      else
        tls_uri="$(_uri_vmess "$email" "$domain" "443" "$net" "$uuid" "$path" "" "1")"
        ntls_uri="$(_uri_vmess "$email" "$domain" "80"  "$net" "$uuid" "$path" "" "0")"
      fi
      ;;
    trojan)
      if [[ "$net" == "grpc" ]]; then
        tls_uri="$(_uri_trojan "$email" "$domain" "443" "$net" "$pass" "$path" "$sname" "1")"
      else
        tls_uri="$(_uri_trojan "$email" "$domain" "443" "$net" "$pass" "$path" "$sname" "1")"
        ntls_uri="$(_uri_trojan "$email" "$domain" "80"  "$net" "$pass" "$path" "$sname" "0")"
      fi
      ;;
  esac

  [[ -n "$tls_uri"  ]] && printf "URI [TLS]: %s\n"  "$tls_uri"
  [[ -n "$ntls_uri" ]] && printf "URI [nTLS]: %s\n" "$ntls_uri"
}
banner_success() {
  local proto="$1" transport="$2" email="$3" show_text="$4" days="$5"
  local add_json="${6:-}"

  get_val() {
    local key="$1"
    printf "%s\n" "$show_text" | awk -v k="$key" '
      tolower(substr($0,1,length(k)))==tolower(k) {
        s=substr($0,length(k)+1); sub(/^[ \t:]+/,"",s); print s; exit
      }'
  }
  mj() { jq -c 'if type=="string" then (try fromjson catch .) else . end'; }

  local inb_line inb_id inb_remark
  inb_line="$(printf "%s\n" "$show_text" | awk 'tolower(substr($0,1,7))=="inbound"{print; exit}')"
  inb_id="$(awk '{for(i=1;i<=NF;i++){if($i=="INBOUND"){print $(i+1); exit}}}' <<<"$inb_line" 2>/dev/null)"
  inb_remark="$(sed -n 's/^\s*INBOUND\s*[0-9]\+\s*\([^()]*\).*/\1/p' <<<"$inb_line" | head -1 | xargs)"

  local uuid pass subid expiry path sname sid pkey flow
  uuid="$(get_val 'UUID')"
  pass="$(get_val 'Password')"
  subid="$(get_val 'SubID')"
  expiry="$(get_val 'Expiry')"
  flow="$(get_val 'Flow')"

local s_psk="" u_psk=""
  if [[ -n "${add_json:-}" ]]; then
    case "$proto" in
      trojan)
        pass="$(jq -r 'try .client.clients[0].password // empty' <<<"$add_json")"
        ;;
      vless|vmess)
        if [[ -z "${uuid:-}" ]]; then
          uuid="$(jq -r 'try .client.clients[0].id // empty' <<<"$add_json")"
        fi
        ;;
      shadowsocks)
        # Ambil PSK dari show_text kalau ada…
        if [[ -z "$s_psk" ]]; then s_psk="$(get_val 'Server PSK')"; fi
        if [[ -z "$u_psk" ]]; then u_psk="$(get_val 'User PSK')"; fi
        # …kalau kosong, fallback ke add_json:
        if [[ -z "$s_psk" ]]; then
          s_psk="$(jq -r 'try .ss2022_combined_psk // empty' <<<"$add_json" 2>/dev/null || true)"
        fi
        if [[ -z "$u_psk" ]]; then
          u_psk="$(jq -r 'try .client.clients[0].password // empty' <<<"$add_json" 2>/dev/null || true)"
        fi
        ;;
    esac
  fi
  path="$(get_val 'Path')"
  sname="$(get_val 'Service Name')"
  sid="$(get_val 'shortid')"
  pkey="$(get_val 'public key')"

  if [[ -n "${inb_id:-}" ]] && { [[ -z "${path:-}" ]] && [[ -z "${sname:-}" ]] && [[ -z "${sid:-}" ]] && [[ -z "${pkey:-}" ]]; }; then
    local API_HOST_FILE=/etc/x-ui/api_host
    local API_PORT_FILE=/etc/x-ui/api_port
    local WEBBASE_FILE=/etc/x-ui/webbasepath
    local USER_FILE=/etc/x-ui/username
    local PASS_FILE=/etc/x-ui/password
    read_file(){ local p="$1" def="${2:-}"; [[ -r "$p" ]] && tr -d '\r' <"$p" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' || printf %s "$def"; }
    norm_basepath(){ local s="${1:-/}"; [[ "$s" != /* ]] && s="/$s"; [[ "$s" != */ ]] && s="$s/"; printf %s "$s"; }
    urlencode(){ local i ch out= LC_ALL=C; for ((i=0;i<${#1};i++)); do ch="${1:i:1}"; case "$ch" in [a-zA-Z0-9.~_-]) out+="$ch";; *) printf -v out '%s%%%02X' "$out" "'$ch";; esac; done; printf %s "$out"; }
    login() {
      local base="$1" user="$2" pass="$3" cookie="$4"
      local ue pe; ue="$(urlencode "$user")"; pe="$(urlencode "$pass")"
      curl -sS -c "$cookie" -b "$cookie" -H 'Content-Type: application/x-www-form-urlencoded' \
        -X POST --data "username=$ue&password=$pe&twoFactorCode=" "${base}login" >/dev/null
    }

    local host port webbase user passw BASE COOKIE
    host="$(read_file "$API_HOST_FILE" 127.0.0.1)"
    port="$(read_file "$API_PORT_FILE" 2053)"
    webbase="$(norm_basepath "$(read_file "$WEBBASE_FILE" /)")"
    user="$(read_file "$USER_FILE")"
    passw="$(read_file "$PASS_FILE")"
    BASE="http://${host}:${port}${webbase}"
    COOKIE="$(mktemp /tmp/3ling_cookie.XXXXXX)"
    trap '[[ -n "${COOKIE:-}" ]] && rm -f "$COOKIE" || true' RETURN
    login "$BASE" "$user" "$passw" "$COOKIE" || true

    local inb_json stream
    inb_json="$(curl -sS -b "$COOKIE" -H 'Accept: application/json' "${BASE}panel/api/inbounds/list" \
                | jq -c '.obj // []' 2>/dev/null)"
    stream="$(jq -c --arg id "$inb_id" '
        map(select((.id|tostring)==$id)) | .[0].streamSettings
      ' <<<"$inb_json" | mj)"

    case "$transport" in
      ws)
        path="$(jq -r '(.wsSettings // {} | .path) // empty' <<<"$stream")"
        ;;
      httpupgrade)
        path="$(jq -r '(.httpupgradeSettings // {} | .path) // empty' <<<"$stream")"
        [[ -z "$path" || "$path" == "null" ]] && path="$(jq -r '(.httpSettings // {} | .path) // empty' <<<"$stream")"
        [[ -z "$path" || "$path" == "null" ]] && path="$(jq -r '(.xhttpSettings // {} | .path) // empty' <<<"$stream")"
        ;;
      xhttp)
        path="$(jq -r '(.xhttpSettings // {} | .path) // empty' <<<"$stream")"
        [[ -z "$path" || "$path" == "null" ]] && path="$(jq -r '(.wsSettings // {} | .path) // empty' <<<"$stream")"
        ;;
      grpc)
        sname="$(jq -r '(.grpcSettings // {} | .serviceName) // empty' <<<"$stream")"
        ;;
      tcp)
        if [[ "$proto" == "vless" ]]; then
          sid="$(jq -r '(.realitySettings // {} | .shortIds // [] | .[0]) // empty' <<<"$stream")"
          pkey="$(jq -r '(.realitySettings.settings // {} | .publicKey) // empty' <<<"$stream")"
        else
          sid=""; pkey=""
        fi
        ;;
    esac
  fi

  local DOMAIN_FILE=/etc/x-ui/domain
  local API_HOST_FILE=/etc/x-ui/api_host
  local domain sublink
  domain="$(read_file "$DOMAIN_FILE" "")"
  [[ -z "$domain" ]] && domain="$(read_file "$API_HOST_FILE" "127.0.0.1")"
  if [[ -n "$domain" && -n "$subid" ]]; then
    sublink="https://${domain}/sub/${subid}"
  else
    sublink="-"
  fi

  local cred_label cred_value
  case "$proto" in
    vless|vmess) cred_label="UUID"; cred_value="${uuid:-"-"}" ;;
    trojan)      cred_label="Password"; cred_value="${pass:-"-"}" ;;
    shadowsocks)
      # Kalau berhasil ambil PSK, tampilkan "ServerPSK:UserPSK"; kalau tidak, fallback ke Password lama
      if [[ -n "$s_psk" || -n "$u_psk" ]]; then
        cred_label="ServerPSK:UserPSK"; cred_value="${s_psk:-"-"}:${u_psk:-"-"}"
      else
        cred_label="Password"; cred_value="${pass:-"-"}"
      fi
      ;;
    *) cred_label="Credential"; cred_value="-" ;;
  esac

  local ports="TLS 443, nTLS 80"
  if [[ "$proto" == "vless" && "$transport" == "tcp" ]]; then
    ports="TLS 443"
  elif [[ "$transport" == "grpc" ]]; then
    ports="TLS 443"
  fi

  local cred_uuid="" cred_pass="" uris
  case "$proto" in
    vless|vmess) cred_uuid="${uuid:-}";;
    trojan)      cred_pass="${pass:-}";;
  esac
  uris="$(print_uris "$proto" "$transport" "$email" "$domain" \
                     "$cred_uuid" "$cred_pass" \
                     "${path:-}" "${sname:-}" "${flow:-}" "${sid:-}" "${pkey:-}" \
                     2>/dev/null || true)"

  local made_at; made_at="$(date '+%Y-%m-%d %H:%M:%S')"
  local banner
  banner="$BORDER
           ⇱ ${proto} ${transport} Account Created ⇲
$BORDER
Username: ${email:-"-"}
Domain: ${domain:-"-"}
${cred_label}: ${cred_value}
Ports: ${ports}
Durasi: ${days:-"-"} hari
Protocol: ${proto}
Transport: ${transport}"
  case "$transport" in
    ws|xhttp)
      banner="${banner}
Path: ${path:-"-"}"
      ;;
    httpupgrade)
      if [[ -n "${path:-}" && "$path" != /* ]]; then
        banner="${banner}
Path: /${path}"
      else
        banner="${banner}
Path: ${path:-"-"}"
      fi
      ;;
    grpc)
      banner="${banner}
Service Name: ${sname:-"-"}"
      ;;
    tcp)
      if [[ "$proto" == "vless" ]]; then
        [[ -n "${flow:-}" ]] && banner="${banner}
Flow: ${flow}"
        banner="${banner}
shortId / publicKey: ${sid:-"-"} / ${pkey:-"-"}"
      else
        banner="${banner}
TCP Options: -"
      fi
      ;;
  esac
# Tambah info cipher utk Shadowsocks 2022
  if [[ "$proto" == "shadowsocks" ]]; then
    banner="${banner}
Encryption: 2022-blake3-aes-128-gcm"
  fi
  banner="${banner}
INBOUND: ${inb_id:-"-"}  ${inb_remark:-"-"}
Subscription: ${sublink:-"-"}
Expired: ${expiry:-"-"}
Dibuat pada: ${made_at}
$BORDER"
  if [[ -n "$uris" ]]; then
    banner="${banner}
${uris}"
  fi

  echo
  echo "$banner"

  if type tg_notify_enabled >/dev/null 2>&1 && tg_notify_enabled; then
    tg_send_title_body "New ${proto}/${transport} account" "$banner"
  fi

  exit 0
}
edit_user_menu() {
  local email choice q n t p expv

  echo
  echo "${C_YELLOW}[Edit User]${C_RESET} — pilih target user dulu."
  email="$(pick_email_from_list)"
  [[ -z "$email" ]] && { echo "Email wajib diisi."; pause; return 1; }

  while true; do
    echo
    echo "Target: ${C_CYAN}${email}${C_RESET}"
    echo "----------------------------------------"
    echo "1) Regenerate UUID/Password"
    echo "2) Ubah Limit Quota (TotalGB)"
    echo "3) Ubah Limit IP"
    echo "4) Pindah Transport (protocol tetap)"
    echo "5) Ganti Protocol + Transport (move)"
    echo "6) Set Expiry"
    echo "7) Cleanup tmp entries (optional)"
    echo "8) Ganti target email"
    echo "0) Kembali"
    read -r -p "Pilih [0-8]: " choice

    case "$choice" in
      1)
        echo "→ Regenerate credential untuk ${email}"
        3ling edit regen "$email"
        3ling show "$email"
        pause
        ;;
      2)
        echo "→ Ubah kuota (GB) untuk ${email}"
        echo "   Gunakan '-' atau '0' untuk unlimited."
        read -r -p "Masukkan kuota (GB atau -): " q
        [[ -z "$q" ]] && { echo "Batal: kuota kosong."; pause; continue; }
        3ling edit quota "$email" "$q"
        3ling show "$email"
        pause
        ;;
      3)
        echo "→ Ubah limit IP untuk ${email}"
        read -r -p "Masukkan limit IP (angka): " n
        [[ -z "$n" ]] && { echo "Batal: limit ip kosong."; pause; continue; }
        3ling edit limitip "$email" "$n"
        3ling show "$email"
        pause
        ;;
      4)
        echo "→ Pindah transport (protocol tetap) untuk ${email}"
        echo "   Pilih transport baru:"
        read -r p t <<<"$(pick_proto_transport)"
        # Pastikan protocol tidak berubah: cek proto sekarang
        # (biar aman, kita pakai transport saja; cmd `transport` memang protocol tetap)
        3ling edit transport "$email" "$t"
        3ling show "$email"
        pause
        ;;
      5)
        echo "→ Ganti protocol + transport untuk ${email}"
        read -r p t <<<"$(pick_proto_transport)"
        3ling edit move "$email" "$p" "$t"
        3ling show "$email"
        pause
        ;;
      6)
        echo "→ Set expiry untuk ${email}"
        echo "   Format:"
        echo "   - angka hari (contoh: 30)"
        echo "   - '-' atau '0' untuk unlimited"
        echo "   - tanggal absolut 'YYYY-MM-DD' atau 'YYYY-MM-DDTHH:MM'"
        read -r -p "Masukkan nilai expiry: " expv
        [[ -z "$expv" ]] && { echo "Batal: nilai expiry kosong."; pause; continue; }
        3ling edit set-exp "$email" "$expv"
        3ling show "$email"
        pause
        ;;
      7)
        echo "→ Cleanup entri sementara (tmp-...) untuk ${email}"
        3ling edit cleanup-email "$email"
        pause
        ;;
      8)
        email="$(pick_email_from_list)"
        [[ -z "$email" ]] && { echo "Email tidak boleh kosong, tetap pakai yang lama."; }
        ;;
      0|"")
        break
        ;;
      *)
        echo "Pilihan tidak dikenal."
        ;;
    esac
  done
}
# ------------------------------ Actions --------------------------------------
action_add() {
  echo
  echo "${C_GREEN}Add user (wizard)${C_RESET}"

  # 1) Remark inbound
  local remark days proto transport quota lip
  remark="$(prompt 'Username')"
  days="$(prompt 'Masa aktif (hari)')"

  # 2) Pilih protocol
  local proto_pick
  proto_pick="$(choose_menu "Pilih Protocol:" "vless" "vmess" "trojan" "shadowsocks")"

  # 3) Pilih transport (difilter)
  local transports=()
  case "$proto_pick" in
    vless)
      transports=("tcp (reality)" "ws" "httpupgrade" "grpc" "xhttp")
      ;;
    vmess)
      transports=("ws" "httpupgrade" "grpc" "xhttp")
      ;;
    trojan)
      transports=("tcp" "ws" "httpupgrade" "grpc" "xhttp")
      ;;
    shadowsocks)
      transports=("ws" "httpupgrade")
      ;;
  esac
  local transport_pick; transport_pick="$(choose_menu "Pilih Transport:" "${transports[@]}")"

  # Normalisasi ke argumen 3ling
  case "$transport_pick" in
    "tcp (reality)") transport="tcp" ;;
    *) transport="$transport_pick" ;;
  esac

  # 4) Opsi quota & limit IP
  quota="$(prompt 'Total quota (GB), "-" untuk unlimited' "-")"
  lip="$(prompt 'Limit IP override (kosong=default)' "")"

  # Panggil 3ling add
  echo
  echo "${C_DIM}>> 3ling add \"$remark\" \"$days\" \"$proto_pick\" \"$transport\" ${quota:+\"$quota\"} ${lip:+\"$lip\"}${C_RESET}"
  set +e
  add_out="$(3ling add "$remark" "$days" "$proto_pick" "$transport" ${quota:+$quota} ${lip:+$lip} 2>&1)"
  add_rc=$?
  set -e
  echo "------------------------------------------------------------"
  echo "$add_out"
  echo "------------------------------------------------------------"

  # Validasi hasil (pastikan JSON ok:true)
  if (( add_rc != 0 )) || ! jq -e '.ok==true' >/devnull 2>&1 <<<"$add_out"; then
    echo "${C_RED}Gagal membuat akun.${C_RESET}"
    pause
    return
  fi

  # Ambil email langsung dari JSON
  local new_email=""
  new_email="$(jq -r '.client.clients[0].email // empty' <<<"$add_out")"

  if [[ -z "$new_email" ]]; then
    read -rp "Masukkan Username/Email untuk tampilkan detail (atau kosong untuk skip): " new_email
  fi

  local details=""
  if [[ -n "$new_email" ]]; then
    set +e
    details="$(3ling show "$new_email" 2>/dev/null)"
    set -e
  fi

  echo
  banner_success "$proto_pick" "$transport" "${new_email:-"-"}" "${details:-""}" "$days" "$add_out"
}

action_delete() {
  echo
  echo "${C_GREEN}Delete user${C_RESET}"

  # pakai picker seperti menu Edit User
  local email; email="$(pick_email_from_list)"
  [[ -z "$email" ]] && { echo "Email wajib diisi."; pause; return; }

  read -rp "Yakin hapus ${email}? [y/N]: " yn
  [[ "${yn,,}" != "y" ]] && { echo "Batal."; pause; return; }

  local out rc
  set +e
  out="$(3ling delete "$email" 2>&1)"; rc=$?
  set -e

  echo "$out"

  # kirim notifikasi Telegram (jika diaktifkan)
  if tg_notify_enabled; then
    local title
    if (( rc==0 )); then
      title="Deleted: ${email}"
    else
      title="Delete FAILED: ${email}"
    fi
    tg_send_json_pretty "$title" "$out"
  fi

  pause
}

action_extend() {
  echo
  echo "${C_GREEN}Extend user${C_RESET}"
  # pakai picker
  local email; email="$(pick_email_from_list)"
  [[ -z "$email" ]] && { echo "Email wajib diisi."; pause; return; }
  local days;  days="$(prompt 'Tambah hari (mis: 7)')"

  local out rc
  set +e
  out="$(3ling extend "$email" "$days" 2>&1)"; rc=$?
  set -e
  echo "$out"

  # ambil detail baru (expiry) untuk dikirim
  local show=""
  set +e; show="$(3ling show "$email" 2>/dev/null)"; set -e

  if tg_notify_enabled; then
    local title
    if (( rc==0 )); then title="Extended ${email} (+${days} hari)"; else title="Extend FAILED: ${email}"; fi
    tg_send_json_pretty "$title" "${show:-$out}"
  fi

  pause
}

action_list() {
  echo
  echo "${C_GREEN}List users${C_RESET}"
  3ling list
  pause
}

action_show() {
  echo
  echo "${C_GREEN}Show user${C_RESET}"
  # pakai picker
  local email; email="$(pick_email_from_list)"
  [[ -z "$email" ]] && { echo "Email wajib diisi."; pause; return; }

  3ling show "$email"
  pause
}

action_toggle_enable() {
  echo
  echo "${C_GREEN}Enable/Disable user${C_RESET}"
  # pakai picker
  local email; email="$(pick_email_from_list)"
  [[ -z "$email" ]] && { echo "Email wajib diisi."; pause; return; }

  echo "1) Enable"
  echo "2) Disable"
  read -rp "Pilih [1-2]: " mode

  local out rc title
  case "${mode:-}" in
    1)
      set +e; out="$(3ling enable "$email" 2>&1)"; rc=$?; set -e
      title="Enabled: ${email}"
      ;;
    2)
      set +e; out="$(3ling disable "$email" 2>&1)"; rc=$?; set -e
      title="Disabled: ${email}"
      ;;
    *)
      echo "${C_RED}Pilihan tidak valid${C_RESET}"
      pause
      return
      ;;
  esac

  echo "$out"

  # opsional kirim status + ringkasan show
  local show=""
  set +e; show="$(3ling show "$email" 2>/dev/null)"; set -e

  if tg_notify_enabled; then
    (( rc==0 )) || title="${title} (FAILED)"
    tg_send_json_pretty "$title" "${show:-$out}"
  fi

  pause
}

action_ceklogin() {
  echo
  echo "${C_GREEN}Cek Login${C_RESET}"
  echo "1) List semua user"
  echo "2) By email"
  read -rp "Pilih [1-2]: " m
  case "${m:-}" in
    1) 3ling ceklogin list ;;
    2) local email; email="$(prompt 'Email')"; run_cmd 3ling ceklogin "$email" ;;
    *) echo "${C_RED}Pilihan tidak valid${C_RESET}" ;;
  esac
  pause
}

action_purge() {
  echo
  echo "${C_GREEN}Purge depleted/expired clients${C_RESET}"
  echo "1) Semua inbound"
  echo "2) Hanya 1 inbound (by ID)"
  read -rp "Pilih [1-2]: " m

  local out rc title

  case "${m:-}" in
    1)
      set +e; out="$(3ling purge 2>&1)"; rc=$?; set -e
      title="Purge: all inbounds"
      ;;
    2)
      local inb; inb="$(prompt 'Inbound ID')"
      set +e; out="$(3ling purge "$inb" 2>&1)"; rc=$?; set -e
      title="Purge: inbound ${inb}"
      ;;
    *)
      echo "${C_RED}Pilihan tidak valid${C_RESET}"
      pause
      return
      ;;
  esac

  echo "$out"

  if tg_notify_enabled; then
    (( rc==0 )) || title="${title} (FAILED)"
    tg_send_json_prety "$title" "$out"
  fi

  pause
}

# ------------------------------ Main Loop ------------------------------------
while :; do
  header
  cat <<MENU123
1) Add user
2) Delete user
3) Extend user (days)
4) List users
5) Show user detail
6) Enable/Disable user
7) Cek login (IP & last online)
8) Purge depleted/expired clients
9) Edit user
0) Exit
MENU123
  echo
  read -rp "Choose: " ch
  case "${ch:-}" in
    1) action_add ;;
    2) action_delete ;;
    3) action_extend ;;
    4) action_list ;;
    5) action_show ;;
    6) action_toggle_enable ;;
    7) action_ceklogin ;;
    8) action_purge ;;
    9) edit_user_menu ;;
    0) echo "backing."; menu ;;
    *) echo "${C_RED}Pilihan tidak valid${C_RESET}"; sleep 1 ;;
  esac
done