#!/usr/bin/env bash
# install-sendBILL.sh — set up and run a chatty APRS message sender with ACK wait
set -euo pipefail

WORKDIR="/home/bbl/html/w1hd-tracker"
PY="$WORKDIR/sendBILL.py"
WRAP="$WORKDIR/sendBILL.sh"

# ---- Defaults (override by exporting before running this script) ----
: "${APRS_CALLSIGN:=W1HD}"
: "${APRS_PASSCODE:=27799}"
: "${APRS_HOST:=noam.aprs2.net}"
: "${APRS_PORT:=14580}"
: "${APRS_DST:=W1HD-9}"
: "${APRS_APP_PROG:=w1hd-sender}"
: "${APRS_APP_VER:=1.2}"
: "${APRS_ACK_WAIT:=180}"
: "${APRS_MSGID_FILE:=$WORKDIR/.msgid}"

MSG="${1:-Hello from Hal — please ACK me!}"

echo "==> Using:"
echo "    CALLSIGN=${APRS_CALLSIGN}  DST=${APRS_DST}"
echo "    HOST=${APRS_HOST}  PORT=${APRS_PORT}"
echo "    APPID='${APRS_APP_PROG} ${APRS_APP_VER}'"
echo "    ACK_WAIT=${APRS_ACK_WAIT}s"
echo

echo "==> Ensuring workdir ${WORKDIR}…"
mkdir -p "$WORKDIR"

echo "==> Ensuring Python venv + aprslib…"
if [ ! -x "$WORKDIR/venv/bin/python" ]; then
  python3 -m venv "$WORKDIR/venv"
fi
# shellcheck disable=SC1091
source "$WORKDIR/venv/bin/activate"
pip install --upgrade pip >/dev/null
pip install --upgrade aprslib >/dev/null

echo "==> Writing ${PY}…"
cat >"$PY" <<'PY'
#!/usr/bin/env python3
import os, sys, time, re, threading, random, pathlib, aprslib

# --------- Config from env ----------
CALLSIGN = os.environ.get("APRS_CALLSIGN", "W1HD")
PASSCODE = os.environ.get("APRS_PASSCODE", "27799")
HOST     = os.environ.get("APRS_HOST", "noam.aprs2.net")
PORT     = int(os.environ.get("APRS_PORT", "14580"))
DST      = os.environ.get("APRS_DST", "W1HD-9")

APP_PROG = os.environ.get("APRS_APP_PROG", "w1hd-sender")
APP_VER  = os.environ.get("APRS_APP_VER",  "1.2")
ACK_WAIT_SEC = int(os.environ.get("APRS_ACK_WAIT", "180"))
STATE_FILE   = os.environ.get("APRS_MSGID_FILE", ".msgid")

def ts():
    return time.strftime("%Y-%m-%dT%H:%M:%S%z")

def note(msg):
    print(f"{ts()}  {msg}", flush=True)

def next_msgid(state_path: str) -> int:
    try:
        p = pathlib.Path(state_path)
        n = int(p.read_text().strip()) if p.exists() else random.randint(0, 99)
    except Exception:
        n = random.randint(0, 99)
    n = (n + 1) % 100
    try:
        pathlib.Path(state_path).write_text(f"{n}\n")
    except Exception:
        pass
    return n

def pad9(callsign: str) -> str:
    return (callsign[:9] + " " * 9)[:9]

def build_raw(src: str, dst: str, text: str, msgid: int) -> str:
    # payload wants :<addressee-9> :<text>{NN   (no closing brace)
    payload = f":{pad9(dst)}:{text}" + "{" + f"{msgid:02d}"
    return f"{src}>APRS,TCPIP*:{payload}\r\n"

def send_message(text: str, msgid: int) -> str:
    rawline = build_raw(CALLSIGN, DST, text, msgid)
    note(f"CONNECT (TX) → {HOST}:{PORT} as {CALLSIGN} (vers {APP_PROG} {APP_VER})")
    tx = aprslib.IS(CALLSIGN, passwd=PASSCODE, host=HOST, port=PORT)
    tx.appid = f"{APP_PROG} {APP_VER}"
    tx.connect()
    note("LOGIN OK; sending APRS message …")
    tx.sendall(rawline)   # push raw frame
    try:
        tx.disconnect()
    except Exception:
        pass
    note(f"TX OK → {DST}  id={msgid:02d}")
    print("RAW:", rawline.strip(), flush=True)
    return rawline

def wait_for_ack(msgid: int, timeout_sec: int) -> bool:
    got_ack = threading.Event()
    msg_re = re.compile(r":(.{9}):(.*)")

    def handler(pkt):
        line = pkt.decode("utf-8", "ignore") if isinstance(pkt, bytes) else str(pkt)
        if line.startswith("#") or ":" not in line or ">" not in line:
            return
        m = msg_re.search(line)
        if not m:
            return
        addressee = m.group(1).rstrip()
        content   = m.group(2).strip()
        if addressee.upper() != CALLSIGN.upper():
            return
        note(f"RX → {CALLSIGN}: {content}")
        m2 = re.search(r"\back(\d{1,2})\b", content, re.IGNORECASE)
        if m2 and int(m2.group(1)) == msgid:
            got_ack.set()

    def rx_thread():
        try:
            rx = aprslib.IS(CALLSIGN, passwd=PASSCODE, host=HOST, port=PORT)
            rx.appid = f"{APP_PROG} {APP_VER}"
            rx.set_filter(f"m/{CALLSIGN}")  # only messages addressed to us
            note(f"CONNECT (RX) → {HOST}:{PORT} with filter m/{CALLSIGN}")
            rx.connect()
            rx.consumer(handler, raw=True)
        except Exception as e:
            note(f"(ack listener note) {e}")

    t = threading.Thread(target=rx_thread, daemon=True)
    t.start()

    start = time.time()
    spinner = "|/-\\"
    i = 0
    while time.time() - start < timeout_sec:
        if got_ack.is_set():
            note(f"ACK RECEIVED: ack{msgid:02d}")
            return True
        left = int(timeout_sec - (time.time() - start))
        sys.stdout.write(f"\rWaiting for ACK (ack{msgid:02d}) {spinner[i%4]}  {left:3d}s ")
        sys.stdout.flush()
        i += 1
        time.sleep(0.25)
    sys.stdout.write("\n")
    return False

def main():
    text = " ".join(sys.argv[1:]).strip() if len(sys.argv) > 1 else "Hello from Hal — please ACK me!"
    msgid = next_msgid(STATE_FILE)

    note(f"SENDING → {DST}  msg='{text}'  id={msgid:02d}")
    try:
        send_message(text, msgid)
    except Exception as e:
        note(f"TX ERROR: {e}")
        sys.exit(2)

    note(f"Waiting up to {ACK_WAIT_SEC}s for ack{msgid:02d} (messages to {CALLSIGN}) …")
    got = wait_for_ack(msgid, ACK_WAIT_SEC)
    if got:
        note("✅ DELIVERED (peer ACKed)")
        sys.exit(0)
    else:
        note("⌛ NO ACK (timeout). Could still be delivered (peer offline / no-ACK client / RF-only).")
        sys.exit(1)

if __name__ == "__main__":
    main()
PY
chmod +x "$PY"

echo "==> Writing wrapper ${WRAP}…"
cat >"$WRAP" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
cd /home/bbl/html/w1hd-tracker
exec ./venv/bin/python ./sendBILL.py "$@"
SH
chmod +x "$WRAP"

echo "==> Running sendBILL now…"
export APRS_CALLSIGN APRS_PASSCODE APRS_HOST APRS_PORT APRS_DST APRS_APP_PROG APRS_APP_VER APRS_ACK_WAIT APRS_MSGID_FILE
exec "$WRAP" "$MSG"
