#!/usr/bin/env python3
import hashlib
import json
import os
import struct
import sys
import time

import serial
from coincurve import PrivateKey


def read_exact(ser: serial.Serial, n: int, timeout_s: float = 8.0) -> bytes:
    end = time.time() + timeout_s
    out = bytearray()
    while len(out) < n and time.time() < end:
        chunk = ser.read(n - len(out))
        if chunk:
            out.extend(chunk)
            continue
        time.sleep(0.005)
    return bytes(out)


def recv_frame(ser: serial.Serial, timeout_s: float = 10.0) -> bytes:
    end = time.time() + timeout_s
    window = bytearray()

    while time.time() < end:
        b = ser.read(1)
        if not b:
            time.sleep(0.005)
            continue
        window.extend(b)
        if len(window) > 4:
            window.pop(0)
        if len(window) < 4:
            continue
        (n,) = struct.unpack(">I", bytes(window))
        # Keep this cap small to avoid getting stuck on false headers in stream noise.
        if n == 0 or n > 64 * 1024:
            continue
        body = read_exact(ser, n, timeout_s=max(0.2, end - time.time()))
        if len(body) != n:
            continue
        if body[:1] not in (b"{", b"["):
            continue
        try:
            json.loads(body.decode("utf-8", errors="strict"))
        except Exception:
            continue
        return body

    return b""


def build_auth_envelope(method: str, params, caller_priv: bytes, label: str = "py-cli") -> dict:
    body_json = json.dumps(params, separators=(",", ":")).encode("utf-8")
    body_hash = hashlib.sha256(body_json).hexdigest()

    sk = PrivateKey(caller_priv)
    caller_pub_x = sk.public_key.format(compressed=True)[1:].hex()

    created_at = int(time.time())
    tags = [
        ["nsigner_rpc", "1"],
        ["nsigner_method", method],
        ["nsigner_body_hash", body_hash],
    ]
    serialized = json.dumps(
        [0, caller_pub_x, created_at, 27235, tags, label],
        separators=(",", ":"),
        ensure_ascii=False,
    ).encode("utf-8")
    event_id = hashlib.sha256(serialized).hexdigest()
    sig = sk.sign_schnorr(bytes.fromhex(event_id), aux_randomness=b"\x00" * 32).hex()

    return {
        "id": event_id,
        "pubkey": caller_pub_x,
        "created_at": created_at,
        "kind": 27235,
        "tags": tags,
        "content": label,
        "sig": sig,
    }


def main() -> int:
    port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyACM0"

    caller_priv = bytes(range(1, 33))
    params = []

    req = {
        "jsonrpc": "2.0",
        "id": "1",
        "method": "get_public_key",
        "params": params,
        "auth": build_auth_envelope("get_public_key", params, caller_priv),
    }
    body = json.dumps(req, separators=(",", ":")).encode("utf-8")
    frame = struct.pack(">I", len(body)) + body

    ser = serial.Serial()
    ser.port = port
    ser.baudrate = 115200
    ser.timeout = 0.2
    ser.rtscts = False
    ser.dsrdtr = False
    # Set modem-control lines before open to avoid auto-reset pulses.
    ser.dtr = False
    ser.rts = False

    with ser:
        # Let the board settle if OS still caused a reconnect.
        time.sleep(3.0)

        for attempt in range(1, 4):
            ser.reset_input_buffer()
            ser.write(frame)
            ser.flush()

            resp_body = recv_frame(ser, timeout_s=10.0)
            if resp_body:
                print(resp_body.decode("utf-8", errors="replace"))
                return 0

            print(f"No framed response received (attempt {attempt}/3)")
            time.sleep(0.5)

        return 1


if __name__ == "__main__":
    raise SystemExit(main())
