#!/usr/bin/env bash
# av — Adom Viewer CLI
# Thin wrapper over the AV HTTP API (port 8771) and mgmt relay (port 8772).
set -euo pipefail

AV_PORT=${AV_PORT:-8771}
AV_MGMT=${AV_MGMT:-8772}
AV_URL="http://127.0.0.1:$AV_PORT"
MGMT_URL="http://127.0.0.1:$AV_MGMT"
SCREENSHOT_DIR="/home/adom/project/project-content/screenshots"

# ── Helpers ──────────────────────────────────────────────────────────

die()  { echo "av: $*" >&2; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "requires $1"; }

need curl
need python3

# POST JSON to the main AV server (8771)
av_post() {
  curl -sf -X POST "$AV_URL" \
    -H 'Content-Type: application/json' \
    -d "$1" 2>/dev/null || die "request failed (is AV server running on port $AV_PORT?)"
}

# POST JSON to the mgmt relay (8772)
mgmt_post() {
  curl -sf -X POST "$MGMT_URL" \
    -H 'Content-Type: application/json' \
    -d "$1" 2>/dev/null || die "request failed (is mgmt relay running on port $AV_MGMT?)"
}

# Pretty-print JSON if --pretty flag or stdout is terminal
json_out() {
  if [[ "${AV_PRETTY:-}" == "1" ]] || [[ -t 1 ]]; then
    python3 -m json.tool 2>/dev/null || cat
  else
    cat
  fi
}

# Detect content type from file extension
detect_type() {
  case "${1,,}" in
    *.svg)                    echo "svg" ;;
    *.png|*.jpg|*.jpeg|*.gif|*.webp) echo "image" ;;
    *.html|*.htm)             echo "html_interactive" ;;
    *.md|*.markdown)          echo "markdown" ;;
    *)                        echo "html" ;;
  esac
}

# Wrap SVG in panzoom HTML
wrap_panzoom() {
  local svg="$1"
  cat <<PZEOF
<!DOCTYPE html><html><head><style>body{margin:0;background:#1a1a2e;display:flex;justify-content:center;align-items:center;min-height:100vh;overflow:hidden}#container{display:inline-block}</style></head><body><div id="container">$svg</div><script src="https://cdn.jsdelivr.net/npm/panzoom@9.4.3/dist/panzoom.min.js"></script><script>panzoom(document.getElementById("container"),{maxZoom:50,minZoom:0.1})</script></body></html>
PZEOF
}

# Build JSON payload with python3 (handles escaping)
build_json() {
  python3 -c "
import json, sys
d = {}
i = 1
args = sys.argv[1:]
while i < len(args):
    k = args[i]; v = args[i+1]
    if v == '__TRUE__': d[k] = True
    elif v == '__FALSE__': d[k] = False
    elif v == '__NULL__': d[k] = None
    elif v.startswith('__INT__'): d[k] = int(v[7:])
    elif v.startswith('__FLOAT__'): d[k] = float(v[9:])
    elif v.startswith('__JSON__'): d[k] = json.loads(v[8:])
    else: d[k] = v
    i += 2
print(json.dumps(d))
" -- "$@"
}

# ── Subcommands ──────────────────────────────────────────────────────

cmd_help() {
  cat <<'EOF'
av — Adom Viewer CLI

DISPLAY
  av push <file> [--title T] [--group G] [--source S]
      Push a file to AV. SVGs get panzoom wrapper. Auto-detects type.
  av push --html <string> [--title T] [--group G]
      Push raw HTML as html_interactive.
  av push --iframe <url> [--title T] [--group G]
      Push an iframe URL.
  av push --svg <file> [--title T] [--group G]
      Push SVG with panzoom (explicit, skips type detection).
  av file <path> [--title T] [--group G]
      Push a file via server-side display_file (server reads it).

TABS
  av status              Show viewer connection status.
  av list                List all open tabs with index and group.
  av switch <index>      Switch to tab at index (0-based).
  av close --title T     Close tabs matching title.
  av close --source S    Close tabs matching source.
  av close --group G     Close all tabs in group.
  av clear               Clear all content.

3D
  av 3d <file.glb> [--title T] [--group G] [--source S]
      Display GLB in basic 3D viewer.
      --glb-source SRC   GLB source hint: tscircuit, kicad, auto (default: auto)
      --view VIEW        Camera preset: front,back,top,bottom,isometric (default: isometric)
      --no-ground        Hide ground plane.
  av camera A,B,R        Set camera (alpha,beta,radius in radians).
  av view <preset>       Set named view (front,back,top,bottom,left,right,isometric).
  av ground [on|off]     Toggle or set ground plane.
  av tour [start|stop]   Start or stop camera tour.

TSCIRCUIT 3-TAB
  av tsci <dir>          Push Schematic + PCB + 3D from a tsci build output dir.
      Auto-detects *-schematic.svg, *-pcb.svg, *.glb files.
      --name NAME        Group label (default: from filename prefix).
      --glb-source SRC   GLB source hint (default: tscircuit).
      --view VIEW        3D camera preset (default: top).

CAPTURE
  av shot [--open]       Screenshot AV panel. --open opens in VS Code.
  av shot --tab          Full browser tab capture.

UTILITY
  av reload              Force browser reload via mgmt relay.
  av serve <file.glb>    Serve GLB file, print URL.
  av msg <json>          Post message to viewer.
  av exec <js-code>      Execute JS in viewer.

FLAGS (global)
  --instance ID          Target a specific AV instance.
  --pretty               Force pretty-print JSON output.

ENVIRONMENT
  AV_PORT                Main API port (default: 8771)
  AV_MGMT                Mgmt relay port (default: 8772)
EOF
}

cmd_status() {
  av_post '{"action":"get_status"}' | json_out
}

cmd_list() {
  # List current tabs from server
  av_post '{"action":"list_tabs"}' | python3 -c "
import sys, json
data = json.load(sys.stdin)
tabs = data.get('tabs', [])
if not tabs:
    print('No tabs open.')
    sys.exit(0)
groups = {}
for tab in tabs:
    g = tab.get('group') or ''
    i = tab['index']
    ct = tab.get('contentType', '?')
    t = tab.get('title', ct)
    src = tab.get('source', '')
    line = f'  [{i}] {t}  ({ct})'
    if src: line += f'  src={src}'
    groups.setdefault(g, []).append(line)
for g, lines in groups.items():
    if g:
        print(f'{g}:')
    for l in lines:
        print(l)
    if g: print()
print(f'{len(tabs)} tab(s)')
"
}

cmd_file() {
  # Push a file via server-side display_file (server reads the file and broadcasts)
  local file="" title="" group="" source="" instance=""
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --title)    title="$2"; shift 2 ;;
      --group)    group="$2"; shift 2 ;;
      --source)   source="$2"; shift 2 ;;
      --instance) instance="$2"; shift 2 ;;
      -*)         die "file: unknown flag $1" ;;
      *)          file="$1"; shift ;;
    esac
  done
  [[ -z "$file" ]] && die "file: no file specified"
  [[ -f "$file" ]] || die "file: not found: $file"
  local abs_file
  abs_file="$(realpath "$file")"
  python3 -c "
import json, sys, urllib.request
payload = {'action': 'display_file', 'file': sys.argv[1]}
if sys.argv[2]: payload['title'] = sys.argv[2]
if sys.argv[3]: payload['group'] = sys.argv[3]
if sys.argv[4]: payload['source'] = sys.argv[4]
if sys.argv[5]: payload['instance'] = sys.argv[5]
d = json.dumps(payload).encode()
req = urllib.request.Request('$AV_URL', d, headers={'Content-Type':'application/json'})
print(urllib.request.urlopen(req).read().decode())
" "$abs_file" "$title" "$group" "$source" "$instance" | json_out
}

cmd_push() {
  local title="" group="" source="" instance="" content_type="" raw_content="" file=""

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --title)    title="$2"; shift 2 ;;
      --group)    group="$2"; shift 2 ;;
      --source)   source="$2"; shift 2 ;;
      --instance) instance="$2"; shift 2 ;;
      --html)     content_type="html_interactive"; raw_content="$2"; shift 2 ;;
      --iframe)   content_type="iframe_url"; raw_content="$2"; shift 2 ;;
      --svg)      content_type="svg_panzoom"; file="$2"; shift 2 ;;
      -*)         die "push: unknown flag $1" ;;
      *)          file="$1"; shift ;;
    esac
  done

  if [[ -n "$raw_content" ]]; then
    # Direct content push (--html or --iframe)
    python3 -c "
import json, sys
content = {'contentType': sys.argv[1], 'content': sys.argv[2]}
if sys.argv[3]: content['title'] = sys.argv[3]
if sys.argv[4]: content['group'] = sys.argv[4]
if sys.argv[5]: content['source'] = sys.argv[5]
payload = {'action': 'display', 'content': content}
if sys.argv[6]: payload['instance'] = sys.argv[6]
print(json.dumps(payload))
" "$content_type" "$raw_content" "$title" "$group" "$source" "$instance" \
    | xargs -0 -I{} curl -sf -X POST "$AV_URL" -H 'Content-Type: application/json' -d '{}' \
    | json_out
    return
  fi

  [[ -z "$file" ]] && die "push: no file specified"
  [[ -f "$file" ]] || die "push: file not found: $file"

  local abs_file
  abs_file="$(realpath "$file")"

  if [[ "$content_type" == "svg_panzoom" ]] || [[ "${file,,}" == *.svg ]]; then
    # SVG → wrap in panzoom HTML → push as html_interactive
    local svg_data html_data
    svg_data="$(cat "$abs_file")"
    html_data="$(wrap_panzoom "$svg_data")"
    python3 -c "
import json, sys
with open(sys.argv[1]) as f: svg = f.read()
html = '''<!DOCTYPE html><html><head><style>body{margin:0;background:#1a1a2e;display:flex;justify-content:center;align-items:center;min-height:100vh;overflow:hidden}#container{display:inline-block}</style></head><body><div id=\"container\">''' + svg + '''</div><script src=\"https://cdn.jsdelivr.net/npm/panzoom@9.4.3/dist/panzoom.min.js\"></script><script>panzoom(document.getElementById(\"container\"),{maxZoom:50,minZoom:0.1})</script></body></html>'''
content = {'contentType': 'html_interactive', 'content': html}
if sys.argv[2]: content['title'] = sys.argv[2]
if sys.argv[3]: content['group'] = sys.argv[3]
if sys.argv[4]: content['source'] = sys.argv[4]
payload = {'action': 'display', 'content': content}
if sys.argv[5]: payload['instance'] = sys.argv[5]
import urllib.request
data = json.dumps(payload).encode()
req = urllib.request.Request('$AV_URL', data=data, headers={'Content-Type':'application/json'})
resp = urllib.request.urlopen(req).read().decode()
print(resp)
" "$abs_file" "$title" "$group" "$source" "$instance" | json_out
  elif [[ "${file,,}" == *.png || "${file,,}" == *.jpg || "${file,,}" == *.jpeg || "${file,,}" == *.gif || "${file,,}" == *.webp ]]; then
    # Image → base64 data URI
    local mime ext="${file,,}"
    case "$ext" in
      *.png)  mime="image/png" ;;
      *.jpg|*.jpeg) mime="image/jpeg" ;;
      *.gif)  mime="image/gif" ;;
      *.webp) mime="image/webp" ;;
    esac
    local b64
    b64="data:$mime;base64,$(base64 -w0 "$abs_file")"
    python3 -c "
import json, sys, urllib.request
content = {'contentType': 'image', 'content': sys.argv[1]}
if sys.argv[2]: content['title'] = sys.argv[2]
if sys.argv[3]: content['group'] = sys.argv[3]
if sys.argv[4]: content['source'] = sys.argv[4]
payload = {'action': 'display', 'content': content}
if sys.argv[5]: payload['instance'] = sys.argv[5]
data = json.dumps(payload).encode()
req = urllib.request.Request('$AV_URL', data=data, headers={'Content-Type':'application/json'})
print(urllib.request.urlopen(req).read().decode())
" "$b64" "$title" "$group" "$source" "$instance" | json_out
  else
    # HTML / Markdown / other → read and push
    local ct
    ct="$(detect_type "$file")"
    python3 -c "
import json, sys, urllib.request
with open(sys.argv[1]) as f: data = f.read()
content = {'contentType': sys.argv[2], 'content': data}
if sys.argv[3]: content['title'] = sys.argv[3]
if sys.argv[4]: content['group'] = sys.argv[4]
if sys.argv[5]: content['source'] = sys.argv[5]
payload = {'action': 'display', 'content': content}
if sys.argv[6]: payload['instance'] = sys.argv[6]
d = json.dumps(payload).encode()
req = urllib.request.Request('$AV_URL', d, headers={'Content-Type':'application/json'})
print(urllib.request.urlopen(req).read().decode())
" "$abs_file" "$ct" "$title" "$group" "$source" "$instance" | json_out
  fi
}

cmd_switch() {
  [[ $# -lt 1 ]] && die "switch: need tab index"
  av_post "{\"action\":\"switch_tab\",\"index\":$1}" | json_out
}

cmd_close() {
  local title="" source="" group="" instance=""
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --title)    title="$2"; shift 2 ;;
      --source)   source="$2"; shift 2 ;;
      --group)    group="$2"; shift 2 ;;
      --instance) instance="$2"; shift 2 ;;
      *)          die "close: unknown arg $1" ;;
    esac
  done

  if [[ -n "$group" ]]; then
    python3 -c "
import json, sys, urllib.request
payload = {'action': 'close_group', 'group': sys.argv[1]}
if sys.argv[2]: payload['instance'] = sys.argv[2]
d = json.dumps(payload).encode()
req = urllib.request.Request('$AV_URL', d, headers={'Content-Type':'application/json'})
print(urllib.request.urlopen(req).read().decode())
" "$group" "$instance" | json_out
  elif [[ -n "$title" || -n "$source" ]]; then
    python3 -c "
import json, sys, urllib.request
payload = {'action': 'close_tabs'}
if sys.argv[1]: payload['title'] = sys.argv[1]
if sys.argv[2]: payload['source'] = sys.argv[2]
if sys.argv[3]: payload['instance'] = sys.argv[3]
d = json.dumps(payload).encode()
req = urllib.request.Request('$AV_URL', d, headers={'Content-Type':'application/json'})
print(urllib.request.urlopen(req).read().decode())
" "$title" "$source" "$instance" | json_out
  else
    die "close: need --title, --source, or --group"
  fi
}

cmd_clear() {
  av_post '{"action":"clear"}' | json_out
}

cmd_3d() {
  local file="" title="" group="" source="" instance=""
  local glb_source="auto" view="isometric" ground="true"

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --title)      title="$2"; shift 2 ;;
      --group)      group="$2"; shift 2 ;;
      --source)     source="$2"; shift 2 ;;
      --glb-source) glb_source="$2"; shift 2 ;;
      --view)       view="$2"; shift 2 ;;
      --no-ground)  ground="false"; shift ;;
      --ground)     ground="true"; shift ;;
      --instance)   instance="$2"; shift 2 ;;
      -*)           die "3d: unknown flag $1" ;;
      *)            file="$1"; shift ;;
    esac
  done

  [[ -z "$file" ]] && die "3d: no GLB file specified"
  [[ -f "$file" ]] || die "3d: file not found: $file"

  local abs_file
  abs_file="$(realpath "$file")"

  # Step 1: serve GLB
  local serve_resp glb_url
  serve_resp="$(av_post "{\"action\":\"serve_glb\",\"glbPath\":\"$abs_file\"}")"
  glb_url="$(echo "$serve_resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['glbUrl'])")"

  # Step 2: show in basic 3D viewer
  python3 -c "
import json, sys, urllib.request
options = {
    'title': sys.argv[2] or '3D',
    'glbSource': sys.argv[3],
    'view': sys.argv[4],
    'ground': sys.argv[5],
}
if sys.argv[6]: options['group'] = sys.argv[6]
payload = {'action': 'show_basic_3d', 'modelUrl': sys.argv[1], 'options': options}
if sys.argv[6]: payload['group'] = sys.argv[6]
if sys.argv[7]: payload['source'] = sys.argv[7]
if sys.argv[8]: payload['instance'] = sys.argv[8]
d = json.dumps(payload).encode()
req = urllib.request.Request('$AV_URL', d, headers={'Content-Type':'application/json'})
print(urllib.request.urlopen(req).read().decode())
" "$glb_url" "$title" "$glb_source" "$view" "$ground" "$group" "$source" "$instance" | json_out
}

cmd_tsci() {
  local dist_dir="" name="" glb_source="tscircuit" view="top" instance=""

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --name)       name="$2"; shift 2 ;;
      --glb-source) glb_source="$2"; shift 2 ;;
      --view)       view="$2"; shift 2 ;;
      --instance)   instance="$2"; shift 2 ;;
      -*)           die "tsci: unknown flag $1" ;;
      *)            dist_dir="$1"; shift ;;
    esac
  done

  [[ -z "$dist_dir" ]] && die "tsci: need dist directory path"
  [[ -d "$dist_dir" ]] || die "tsci: directory not found: $dist_dir"

  local abs_dir
  abs_dir="$(realpath "$dist_dir")"

  # Auto-detect files: tsci build outputs *-schematic.svg, *-pcb.svg, *.glb
  # Also supports *.circuit-schematic.svg, *.circuit-pcb.svg, *.circuit.glb
  local sch_svg="" pcb_svg="" glb=""

  # Find schematic SVG
  for f in "$abs_dir"/*-schematic.svg "$abs_dir"/*schematic*.svg; do
    [[ -f "$f" ]] && { sch_svg="$f"; break; }
  done

  # Find PCB SVG
  for f in "$abs_dir"/*-pcb.svg "$abs_dir"/*pcb*.svg; do
    [[ -f "$f" ]] && { pcb_svg="$f"; break; }
  done

  # Find GLB (prefer *.circuit.glb, then any *.glb)
  for f in "$abs_dir"/*.circuit.glb "$abs_dir"/*.glb; do
    [[ -f "$f" ]] && { glb="$f"; break; }
  done

  [[ -n "$sch_svg" ]] || die "tsci: no *schematic*.svg found in $abs_dir"
  [[ -n "$pcb_svg" ]] || die "tsci: no *pcb*.svg found in $abs_dir"
  [[ -n "$glb" ]]     || die "tsci: no *.glb found in $abs_dir"

  # Auto-detect name from filename prefix or directory name
  if [[ -z "$name" ]]; then
    # Extract prefix from schematic filename: "Foo-schematic.svg" → "Foo"
    local base_name
    base_name="$(basename "$sch_svg")"
    # Strip -schematic.svg or .circuit-schematic.svg
    name="${base_name%-schematic.svg}"
    name="${name%.circuit}"
    # If that left us with nothing useful, use directory name
    [[ -z "$name" || "$name" == "$base_name" ]] && name="$(basename "$abs_dir")"
  fi

  echo "Files: $(basename "$sch_svg"), $(basename "$pcb_svg"), $(basename "$glb")"
  echo "Group: $name"

  # Close existing group and wait for viewer to process removal
  av_post "{\"action\":\"close_group\",\"group\":\"$name\"}" >/dev/null 2>&1 || true
  sleep 0.3

  local inst_arg=""
  [[ -n "$instance" ]] && inst_arg="$instance"

  # Push Schematic → PCB → 3D (in that order)
  python3 -c "
import json, sys, urllib.request

AV = '$AV_URL'
name = sys.argv[1]
sch_path = sys.argv[2]
pcb_path = sys.argv[3]
glb_path = sys.argv[4]
glb_source = sys.argv[5]
view = sys.argv[6]
instance = sys.argv[7]

def post(payload):
    if instance:
        payload['instance'] = instance
    d = json.dumps(payload).encode()
    req = urllib.request.Request(AV, d, headers={'Content-Type':'application/json'})
    return json.loads(urllib.request.urlopen(req).read().decode())

def push_svg(svg_path, title):
    with open(svg_path) as f:
        svg = f.read()
    html = '<!DOCTYPE html><html><head><style>body{margin:0;background:#1a1a2e;display:flex;justify-content:center;align-items:center;min-height:100vh;overflow:hidden}#container{display:inline-block}</style></head><body><div id=\"container\">' + svg + '</div><script src=\"https://cdn.jsdelivr.net/npm/panzoom@9.4.3/dist/panzoom.min.js\"></script><script>panzoom(document.getElementById(\"container\"),{maxZoom:50,minZoom:0.1})</script></body></html>'
    content = {'contentType':'html_interactive','content':html,'title':title,'group':name}
    return post({'action':'display','content':content})

# 1. Schematic
r1 = push_svg(sch_path, 'Schematic')
print(f'Schematic: {r1.get(\"viewerCount\",0)} viewer(s)')

# 2. PCB
r2 = push_svg(pcb_path, 'PCB')
print(f'PCB: {r2.get(\"viewerCount\",0)} viewer(s)')

# 3. 3D — serve then show
r3 = post({'action':'serve_glb','glbPath':glb_path})
glb_url = r3['glbUrl']
opts = {
    'title': '3D',
    'glbSource': glb_source,
    'view': view,
    'ground': 'false',
    'group': name,
}
# tscircuit default camera: top/front ~23° tilt (overrides view preset angles)
if glb_source == 'tscircuit':
    opts['alpha'] = 1.57
    opts['beta'] = 0.4
r4 = post({
    'action':'show_basic_3d',
    'modelUrl': glb_url,
    'options': opts,
    'group': name,
})
print(f'3D: {r4.get(\"viewerCount\",0)} viewer(s), loaded={r4.get(\"loaded\",False)}')

print(f'\\nGroup \"{name}\" — 3 tabs pushed')
" "$name" "$sch_svg" "$pcb_svg" "$glb" "$glb_source" "$view" "$inst_arg"
}

cmd_camera() {
  [[ $# -lt 1 ]] && die "camera: need alpha,beta,radius"
  IFS=',' read -r alpha beta radius <<< "$1"
  python3 -c "
import json, sys, urllib.request
payload = {'action':'set_camera'}
if sys.argv[1]: payload['alpha'] = float(sys.argv[1])
if sys.argv[2]: payload['beta'] = float(sys.argv[2])
if sys.argv[3]: payload['radius'] = float(sys.argv[3])
d = json.dumps(payload).encode()
req = urllib.request.Request('$AV_URL', d, headers={'Content-Type':'application/json'})
print(urllib.request.urlopen(req).read().decode())
" "$alpha" "$beta" "$radius" | json_out
}

cmd_view() {
  [[ $# -lt 1 ]] && die "view: need preset name (front,back,top,bottom,left,right,isometric)"
  av_post "{\"action\":\"set_view\",\"view\":\"$1\"}" | json_out
}

cmd_ground() {
  if [[ $# -eq 0 ]]; then
    # Toggle
    av_post '{"action":"set_ground"}' | json_out
  else
    case "$1" in
      on|true|1)   av_post '{"action":"set_ground","visible":true}' | json_out ;;
      off|false|0) av_post '{"action":"set_ground","visible":false}' | json_out ;;
      *) die "ground: use on/off" ;;
    esac
  fi
}

cmd_tour() {
  case "${1:-}" in
    stop) av_post '{"action":"stop_tour"}' | json_out ;;
    start) av_post '{"action":"start_tour"}' | json_out ;;
    *) die "tour: use start/stop" ;;
  esac
}

cmd_shot() {
  local tab=false open=false
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --tab)  tab=true; shift ;;
      --open) open=true; shift ;;
      *) die "shot: unknown flag $1" ;;
    esac
  done

  local resp filepath
  if $tab; then
    resp="$(av_post '{"action":"tab_capture"}')"
  else
    resp="$(mgmt_post '{"action":"screenshot"}')"
  fi

  filepath="$(echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin).get('filePath',''))" 2>/dev/null)"

  if [[ -n "$filepath" && -f "$filepath" ]]; then
    echo "$filepath"
    if $open; then
      code-server "$filepath" 2>/dev/null || true
    fi
  else
    echo "$resp" | json_out
  fi
}

cmd_reload() {
  mgmt_post '{"action":"reload"}' | json_out
}

cmd_serve() {
  [[ $# -lt 1 ]] && die "serve: need GLB file path"
  [[ -f "$1" ]] || die "serve: file not found: $1"
  local abs
  abs="$(realpath "$1")"
  av_post "{\"action\":\"serve_glb\",\"glbPath\":\"$abs\"}" | json_out
}

cmd_msg() {
  [[ $# -lt 1 ]] && die "msg: need JSON message"
  av_post "{\"action\":\"post_message\",\"message\":$1}" | json_out
}

cmd_exec() {
  [[ $# -lt 1 ]] && die "exec: need JS code"
  python3 -c "
import json, sys, urllib.request
payload = {'action':'exec','code':sys.argv[1]}
d = json.dumps(payload).encode()
req = urllib.request.Request('$AV_URL', d, headers={'Content-Type':'application/json'})
print(urllib.request.urlopen(req).read().decode())
" "$1" | json_out
}

# ── Dispatch ─────────────────────────────────────────────────────────

# Handle global flags
while [[ $# -gt 0 ]]; do
  case "$1" in
    --pretty)   export AV_PRETTY=1; shift ;;
    --instance) export AV_INSTANCE="$2"; shift 2 ;;
    *)          break ;;
  esac
done

cmd="${1:-help}"
shift 2>/dev/null || true

case "$cmd" in
  help|--help|-h) cmd_help ;;
  status|st)      cmd_status "$@" ;;
  list|ls)        cmd_list "$@" ;;
  push|p)         cmd_push "$@" ;;
  file)           cmd_file "$@" ;;
  switch|sw)      cmd_switch "$@" ;;
  close|cl)       cmd_close "$@" ;;
  clear)          cmd_clear "$@" ;;
  3d)             cmd_3d "$@" ;;
  tsci)           cmd_tsci "$@" ;;
  camera|cam)     cmd_camera "$@" ;;
  view)           cmd_view "$@" ;;
  ground)         cmd_ground "$@" ;;
  tour)           cmd_tour "$@" ;;
  shot)           cmd_shot "$@" ;;
  reload)         cmd_reload "$@" ;;
  serve)          cmd_serve "$@" ;;
  msg)            cmd_msg "$@" ;;
  exec)           cmd_exec "$@" ;;
  *)              die "unknown command: $cmd (try 'av help')" ;;
esac
