#!/usr/bin/env bash
# Bluesky / AT Protocol desk API · BigGrinRTX employee workspace
# Public reads need no auth. Writes need App Password (not main password).
#
#   bsky status|whoami|profile [handle]|feed [handle]|timeline
#   bsky post "text…"
#   bsky follow handle
#   bsky search "query"
#   bsky session-test
#
# Creds (first hit):
#   $BSKY_HANDLE + $BSKY_APP_PASSWORD
#   ~/.config/biggrinrtx/bsky.handle + bsky.app.pass (mode 600)
#
# Create App Password: https://bsky.app/settings/app-passwords
# Public app:          https://bsky.app/profile/biggrinrtx.bsky.social
#
set -euo pipefail

CFG="${XDG_CONFIG_HOME:-$HOME/.config}/biggrinrtx"
HANDLE_FILE="$CFG/bsky.handle"
PASS_FILE="$CFG/bsky.app.pass"
PDS="${BSKY_PDS:-https://bsky.social}"
PUBLIC="${BSKY_PUBLIC:-https://public.api.bsky.app}"
DEFAULT_HANDLE="${BSKY_HANDLE:-biggrinrtx.bsky.social}"
UA="BGRTX-Bsky-Desk/1.0"

mkdir -p "$CFG"

handle_get() {
  if [[ -n "${BSKY_HANDLE:-}" ]]; then printf '%s' "$BSKY_HANDLE"; return; fi
  if [[ -f "$HANDLE_FILE" ]]; then tr -d '\r\n' <"$HANDLE_FILE"; return; fi
  printf '%s' "$DEFAULT_HANDLE"
}

pass_get() {
  if [[ -n "${BSKY_APP_PASSWORD:-}" ]]; then printf '%s' "$BSKY_APP_PASSWORD"; return; fi
  if [[ -f "$PASS_FILE" ]]; then
    local m; m=$(stat -c '%a' "$PASS_FILE" 2>/dev/null || echo 777)
    if [[ "$m" != "600" && "$m" != "400" ]]; then
      echo "WARN: $PASS_FILE mode $m (want 600)" >&2
    fi
    tr -d '\r\n' <"$PASS_FILE"
    return
  fi
  return 1
}

jget() {
  local url="$1"
  curl -sS --max-time 25 -A "$UA" -H 'Accept: application/json' "$url"
}

cmd_status() {
  local h; h=$(handle_get)
  echo "=== Bluesky desk · $(date -Is) ==="
  echo "handle_default=$h"
  echo "pds=$PDS"
  echo "public=$PUBLIC"
  if pass_get >/dev/null 2>&1; then
    echo "app_password: IN ($PASS_FILE or env) · value never printed"
  else
    echo "app_password: OFF"
    echo "  1) open https://bsky.app/settings/app-passwords"
    echo "  2) Add App Password · name it GrokDesk"
    echo "  3) install -m 600 /dev/stdin $PASS_FILE <<<'xxxx-xxxx-xxxx-xxxx'"
    echo "  4) echo '$h' > $HANDLE_FILE && chmod 600 $HANDLE_FILE"
  fi
  echo
  echo "--- public profile ---"
  cmd_profile "$h" || true
  echo
  echo "profile_url: https://bsky.app/profile/$h"
  echo "settings:    https://bsky.app/settings"
  echo "app_pass:    https://bsky.app/settings/app-passwords"
}

cmd_profile() {
  local actor="${1:-$(handle_get)}"
  jget "$PUBLIC/xrpc/app.bsky.actor.getProfile?actor=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$actor")" \
    | python3 -c '
import json,sys
d=json.load(sys.stdin)
if d.get("error"):
  print("ERR", d.get("error"), d.get("message")); sys.exit(1)
print("handle:   ", d.get("handle"))
print("did:      ", d.get("did"))
print("display:  ", d.get("displayName") or "(none)")
print("posts:    ", d.get("postsCount"))
print("followers:", d.get("followersCount"))
print("follows:  ", d.get("followsCount"))
print("created:  ", d.get("createdAt"))
desc=(d.get("description") or "").strip().replace("\n"," ")
if desc: print("bio:      ", desc[:200])
'
}

cmd_feed() {
  local actor="${1:-$(handle_get)}"
  local lim="${2:-8}"
  jget "$PUBLIC/xrpc/app.bsky.feed.getAuthorFeed?actor=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$actor")&limit=$lim" \
    | python3 -c '
import json,sys
d=json.load(sys.stdin)
if d.get("error"):
  print("ERR", d); sys.exit(1)
feed=d.get("feed") or []
if not feed:
  print("(no posts yet)")
for i,it in enumerate(feed):
  p=it.get("post") or {}
  rec=p.get("record") or {}
  print(f"--- {i+1} ---")
  print(p.get("uri"))
  print((rec.get("text") or "")[:280])
  print("likes", (p.get("likeCount") or 0), "replies", (p.get("replyCount") or 0))
'
}

cmd_search() {
  local q="${*:-biggrinrtx}"
  jget "$PUBLIC/xrpc/app.bsky.actor.searchActors?q=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$q")&limit=8" \
    | python3 -c '
import json,sys
d=json.load(sys.stdin)
for a in d.get("actors") or []:
  print(f"@{a.get(\"handle\")}  {a.get(\"displayName\") or \"\"}  {a.get(\"did\")}")
'
}

session_create() {
  local h p
  h=$(handle_get)
  p=$(pass_get) || { echo "need App Password · see bsky status" >&2; return 1; }
  python3 - "$PDS" "$h" "$p" <<'PY'
import json,sys,urllib.request
pds,h,pw=sys.argv[1],sys.argv[2],sys.argv[3]
req=urllib.request.Request(
  pds.rstrip("/")+"/xrpc/com.atproto.server.createSession",
  data=json.dumps({"identifier":h,"password":pw}).encode(),
  headers={"Content-Type":"application/json","User-Agent":"BGRTX-Bsky-Desk/1.0"},
  method="POST",
)
with urllib.request.urlopen(req, timeout=30) as r:
  print(r.read().decode())
PY
}

cmd_session_test() {
  local s
  s=$(session_create) || exit 1
  python3 -c '
import json,sys
d=json.loads(sys.argv[1])
print("OK session")
print(" handle:", d.get("handle"))
print(" did:   ", d.get("did"))
print(" accessJwt: present" if d.get("accessJwt") else " accessJwt: MISSING")
print(" refreshJwt: present" if d.get("refreshJwt") else " refreshJwt: MISSING")
' "$s"
}

cmd_post() {
  local text="$*"
  [[ -n "$text" ]] || { echo "usage: bsky post \"text\"" >&2; exit 2; }
  local s
  s=$(session_create) || exit 1
  python3 - "$PDS" "$s" "$text" <<'PY'
import json,sys,urllib.request,datetime
pds,sess_raw,text=sys.argv[1],sys.argv[2],sys.argv[3]
sess=json.loads(sess_raw)
did=sess["did"]
jwt=sess["accessJwt"]
now=datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
# truncate usecs to ms-ish acceptable by many PDS
now=now[:-4]+"Z" if now.endswith("Z") else now
record={
  "$type":"app.bsky.feed.post",
  "text": text[:300],
  "createdAt": datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00","Z"),
}
body={
  "repo": did,
  "collection": "app.bsky.feed.post",
  "record": record,
}
req=urllib.request.Request(
  pds.rstrip("/")+"/xrpc/com.atproto.repo.createRecord",
  data=json.dumps(body).encode(),
  headers={
    "Content-Type":"application/json",
    "Authorization": f"Bearer {jwt}",
    "User-Agent":"BGRTX-Bsky-Desk/1.0",
  },
  method="POST",
)
with urllib.request.urlopen(req, timeout=30) as r:
  d=json.loads(r.read().decode())
print("OK posted")
print(" uri:", d.get("uri"))
print(" cid:", d.get("cid"))
# web url best-effort
handle=sess.get("handle") or ""
# rkey from uri at://did/app.bsky.feed.post/RKEY
uri=d.get("uri") or ""
rkey=uri.rsplit("/",1)[-1] if uri else ""
if handle and rkey:
  print(" web: https://bsky.app/profile/%s/post/%s" % (handle, rkey))
PY
}

cmd_timeline() {
  # needs auth
  local s
  s=$(session_create) || exit 1
  python3 - "$PDS" "$s" <<'PY'
import json,sys,urllib.request
pds,sess_raw=sys.argv[1],sys.argv[2]
sess=json.loads(sess_raw)
jwt=sess["accessJwt"]
req=urllib.request.Request(
  pds.rstrip("/")+"/xrpc/app.bsky.feed.getTimeline?limit=10",
  headers={"Authorization": f"Bearer {jwt}", "User-Agent":"BGRTX-Bsky-Desk/1.0"},
)
with urllib.request.urlopen(req, timeout=30) as r:
  d=json.loads(r.read().decode())
for i,it in enumerate(d.get("feed") or []):
  p=it.get("post") or {}
  rec=p.get("record") or {}
  author=(p.get("author") or {}).get("handle")
  print(f"--- {i+1} @{author} ---")
  print((rec.get("text") or "")[:240])
PY
}

cmd_whoami() {
  cmd_profile "$(handle_get)"
}

cmd_help() {
  cat <<EOF
bsky · Bluesky desk (AT Protocol) · BigGrinRTX

  status              handle + public profile + app-pass presence
  whoami|profile [h]  public profile
  feed [handle] [n]   author feed (public)
  search QUERY        find actors (public)
  session-test        login with App Password
  post "text"         create post (needs App Password)
  timeline            home timeline (needs App Password)

App Password (required for write):
  https://bsky.app/settings/app-passwords
  install -m 600 /dev/stdin ~/.config/biggrinrtx/bsky.app.pass <<<'xxxx-xxxx-xxxx-xxxx'
  echo biggrinrtx.bsky.social > ~/.config/biggrinrtx/bsky.handle && chmod 600 ~/.config/biggrinrtx/bsky.handle

Public:
  https://bsky.app/profile/biggrinrtx.bsky.social
EOF
}

cmd="${1:-status}"
shift || true
case "$cmd" in
  status|st) cmd_status ;;
  whoami|me) cmd_whoami ;;
  profile|who) cmd_profile "${1:-}" ;;
  feed) cmd_feed "${1:-}" "${2:-8}" ;;
  search|find) cmd_search "$@" ;;
  session-test|login-test|auth) cmd_session_test ;;
  post|publish) cmd_post "$@" ;;
  timeline|home) cmd_timeline ;;
  help|-h|--help) cmd_help ;;
  *) cmd_help; exit 2 ;;
esac
