# chatcli — uTun headless chat CLI client
#
# Usage:
#   chatcli [--host HOST] [--port PORT] <command> [args...]
#
# Environment:
#   CHATCLI_HOST  — default host (default: 127.0.0.1)
#   CHATCLI_PORT  — default port (default: 9999)
#
# Commands (see chatcli_commands.txt for full description):
#   ping                             alive check
#   status                           node info, connections, NTP
#   channels                         list all channels
#   members <ch_id>                  list members with full state
#   messages <ch_id> [count]         read last messages
#   send <ch_id> <text>              send text message
#   attach <ch_id> <path> [--video]  send a file (or video) as attachment
#   invite <ch_id> [node_id]         create invite link (auto best node by default)
#   invite_nodes <ch_id>             list reachable nodes for invite
#   connect <utun://...>             join channel via invite link
#   create <name>                    create new channel
#   invite_to <ch_id> <node_id> <pubkey_hex> <ip:port> [proto]
#                                    invite a node to add our channel
#   dm_start <node_id> [ch_id]       start direct chat (DM) with a group member
#   dm_list                          list DM conversations
#   dm_messages <conv_id> [count]    read DM messages (decrypted)
#   dm_send <conv_id> <text>         send DM text message
#   listen                           interactive event listener

import sys, os, json, socket, struct

DEFAULT_HOST = os.environ.get("CHATCLI_HOST", "127.0.0.1")
DEFAULT_PORT = int(os.environ.get("CHATCLI_PORT", "9999"))
TIMEOUT = 3

def _connect():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(TIMEOUT)
    try:
        s.connect((DEFAULT_HOST, DEFAULT_PORT))
    except (socket.timeout, ConnectionRefusedError, OSError) as e:
        print(f"ERROR: cannot connect to {DEFAULT_HOST}:{DEFAULT_PORT} — {e}", file=sys.stderr)
        sys.exit(1)
    return s

def _readline(s):
    buf = b""
    while True:
        try:
            ch = s.recv(1)
        except socket.timeout:
            break
        if not ch:
            break
        if ch == b"\n":
            break
        buf += ch
    return buf.decode("utf-8", errors="replace")

def _send(s, js):
    s.sendall(js.encode())

def _req(cmd, **params):
    s = _connect()
    js = {"id": 1, "cmd": cmd}
    js.update(params)
    _send(s, json.dumps(js, ensure_ascii=True) + "\n")
    resp = _readline(s)
    s.close()
    try:
        obj = json.loads(resp)
    except json.JSONDecodeError:
        print(f"RAW: {resp}")
        return
    if obj.get("ok"):
        data = obj.get("data", {})
        if isinstance(data, str):
            print(data)
        else:
            print(json.dumps(data, indent=2, ensure_ascii=False))
    else:
        print(f"ERROR: {obj.get('error', 'unknown')}", file=sys.stderr)
        sys.exit(1)

def cmd_ping():
    _req("ping")

def cmd_status():
    _req("status")

def cmd_channels():
    _req("channels")

def cmd_members(ch_id):
    _req("members", ch=ch_id)

def cmd_messages(ch_id, count=None):
    params = {"ch": ch_id}
    if count:
        params["count"] = int(count)
    _req("messages", **params)

def cmd_send(ch_id, text):
    _req("send", ch=ch_id, content_type="text", data=text)

def cmd_attach(ch_id, path, as_video=False, content_type=None):
    params = {"ch": ch_id, "path": path, "as_video": 1 if as_video else 0}
    if content_type:
        params["content_type"] = content_type
    _req("attach", **params)

def cmd_invite(ch_id, node_id=None):
    params = {"ch": ch_id}
    if node_id:
        params["node_id"] = node_id
    _req("invite", **params)

def cmd_invite_nodes(ch_id):
    _req("invite_nodes", ch=ch_id)

def cmd_connect(link):
    _req("connect", link=link)

def cmd_create(name):
    _req("create_channel", name=name)

def cmd_invite_to(ch_id, node_id, pubkey, addr, proto=None):
    params = {"ch": ch_id, "node_id": node_id, "pubkey": pubkey, "addr": addr}
    if proto:
        params["proto"] = int(proto)
    _req("invite_to", **params)

def cmd_dm_start(node_id, ch=None):
    params = {"node_id": node_id}
    if ch:
        params["ch"] = ch
    _req("dm_start", **params)

def cmd_dm_list():
    _req("dm_list")

def cmd_dm_messages(conv_id, count=None):
    params = {"conv_id": conv_id}
    if count:
        params["count"] = int(count)
    _req("dm_messages", **params)

def cmd_dm_send(conv_id, text):
    _req("dm_send", conv_id=conv_id, data=text)

def cmd_listen():
    print(f"Listening on {DEFAULT_HOST}:{DEFAULT_PORT} (Ctrl+C to quit)")
    s = _connect()
    _send(s, json.dumps({"id": 1, "cmd": "subscribe", "enable": 1}) + "\n")
    try:
        while True:
            line = _readline(s)
            if not line:
                break
            try:
                obj = json.loads(line)
            except json.JSONDecodeError:
                print(line)
                continue
            evt = obj.get("event", "")
            if evt == "msg":
                print(f"[{obj.get('ch','?')}] {obj.get('author_id','?')}:  new message")
            elif evt == "members_changed":
                print(f"[{obj.get('ch','?')}] members changed")
            elif evt == "channel_updated":
                print(f"[{obj.get('ch','?')}] channel updated")
            elif evt == "invite_received":
                print(f"INVITE: ch={obj.get('ch','?')} name={obj.get('ch_name','?')} from={obj.get('from_id','?')}")
            elif evt == "dm_msg":
                print(f"DM[{obj.get('conv','?')}] from={obj.get('author_id','?')}: new message")
            elif evt == "dm_conv":
                print(f"DM[{obj.get('conv','?')}] conversation updated")
            else:
                print(json.dumps(obj, indent=2, ensure_ascii=False))
    except KeyboardInterrupt:
        print("\nDisconnected.")
    finally:
        s.close()

def main():
    args = sys.argv[1:]
    i = 0
    while i < len(args):
        if args[i] == "--host" and i + 1 < len(args):
            global DEFAULT_HOST; DEFAULT_HOST = args[i + 1]; i += 2
        elif args[i] == "--port" and i + 1 < len(args):
            global DEFAULT_PORT; DEFAULT_PORT = int(args[i + 1]); i += 2
        else:
            break
    args = args[i:]

    if not args or args[0] in ("help", "--help", "-h"):
        print(__doc__)
        sys.exit(0)

    cmd = args[0]
    try:
        if cmd == "ping":              cmd_ping()
        elif cmd == "status":          cmd_status()
        elif cmd == "channels":        cmd_channels()
        elif cmd == "members":         cmd_members(*args[1:2] if len(args) > 1 else (_die("usage: members <ch_id>"),))
        elif cmd in ("messages","msgs"): cmd_messages(*args[1:3] if len(args) > 1 else (_die("usage: messages <ch_id> [count]"),))
        elif cmd == "send":            cmd_send(*args[1:3] if len(args) > 2 else (_die("usage: send <ch_id> <text>"),))
        elif cmd == "attach":
            if len(args) < 3:
                _die("usage: attach <ch_id> <path> [--video]")
            as_video = "--video" in args
            path = args[2]
            cmd_attach(args[1], path, as_video)
        elif cmd == "invite":          cmd_invite(*args[1:3] if len(args) > 1 else (_die("usage: invite <ch_id> [node_id]"),))
        elif cmd == "invite_nodes":    cmd_invite_nodes(*args[1:2] if len(args) > 1 else (_die("usage: invite_nodes <ch_id>"),))
        elif cmd in ("connect","join"): cmd_connect(*args[1:2] if len(args) > 1 else (_die("usage: connect <utun://...>"),))
        elif cmd == "create":          cmd_create(*args[1:2] if len(args) > 1 else (_die("usage: create <name>"),))
        elif cmd == "invite_to":       cmd_invite_to(*args[1:6] if len(args) > 4 else (_die("usage: invite_to <ch_id> <node_id> <pubkey_hex> <ip:port> [proto]"),))
        elif cmd == "dm_start":        cmd_dm_start(*args[1:3] if len(args) > 1 else (_die("usage: dm_start <node_id> [ch_id]"),))
        elif cmd == "dm_list":         cmd_dm_list()
        elif cmd in ("dm_messages","dm_msgs"): cmd_dm_messages(*args[1:3] if len(args) > 1 else (_die("usage: dm_messages <conv_id> [count]"),))
        elif cmd == "dm_send":         cmd_dm_send(*args[1:3] if len(args) > 2 else (_die("usage: dm_send <conv_id> <text>"),))
        elif cmd == "listen":          cmd_listen()
        else:                          print(f"Unknown command: {cmd}\nUse --help for usage", file=sys.stderr); sys.exit(1)
    except socket.timeout:
        print(f"ERROR: timeout ({TIMEOUT}s) connecting to {DEFAULT_HOST}:{DEFAULT_PORT}", file=sys.stderr)
        sys.exit(1)
    except ConnectionRefusedError:
        print(f"ERROR: connection refused at {DEFAULT_HOST}:{DEFAULT_PORT} — is uTun running with headless_control_bind?", file=sys.stderr)
        sys.exit(1)

def _die(msg):
    print(msg, file=sys.stderr); sys.exit(1)

if __name__ == "__main__":
    main()
