// Command icomnettest is an iteration probe for the Icom IP remote protocol —
// the LAN server built into the IC-7610 that the Icom "Remote Utility" (and
// wfview) talk to. OpsLog reimplements this directly so it can BE both the
// Remote Utility (Ethernet ↔ radio) and the logger/CAT client, dropping the
// virtual-COM + RS-BA1 chain entirely.
//
// This probe drives TWO streams and hex-dumps everything:
//   Control (UDP 50001): handshake → login → token   [VERIFIED on the real rig]
//   CI-V    (UDP 50002): handshake → openClose(open) → send CI-V read-freq
//                        (FE FE 98 E0 03 FD) → print the rig's reply.
// Framing (passcode table, packet offsets, CI-V data_packet, openclose) is
// reimplemented from the public wfview protocol and verified byte-for-byte
// against real Remote-Utility captures (build/bin/civ*.pcapng). No GPLv3 code.
//
// Usage:
//   go run ./cmd/icomnettest <rig-ip> <user> <pass> [compname]
//
// SAFE: read-only CI-V (operating frequency). No TX, no writes.
package main

import (
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"net"
	"os"
	"time"
)

var le = binary.LittleEndian
var be = binary.BigEndian

// passcodeSeq — Icom's obfuscation table (values live at index 0x20..0x7e).
// VERIFIED: user "f6bgc" → 3F 65 50 25 55 (matches the capture).
var passcodeSeq = [256]byte{
	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
	0x47, 0x5d, 0x4c, 0x42, 0x66, 0x20, 0x23, 0x46, 0x4e, 0x57, 0x45, 0x3d, 0x67, 0x76, 0x60, 0x41, 0x62, 0x39, 0x59, 0x2d, 0x68, 0x7e,
	0x7c, 0x65, 0x7d, 0x49, 0x29, 0x72, 0x73, 0x78, 0x21, 0x6e, 0x5a, 0x5e, 0x4a, 0x3e, 0x71, 0x2c, 0x2a, 0x54, 0x3c, 0x3a, 0x63, 0x4f,
	0x43, 0x75, 0x27, 0x79, 0x5b, 0x35, 0x70, 0x48, 0x6b, 0x56, 0x6f, 0x34, 0x32, 0x6c, 0x30, 0x61, 0x6d, 0x7b, 0x2f, 0x4b, 0x64, 0x38,
	0x2b, 0x2e, 0x50, 0x40, 0x3f, 0x55, 0x33, 0x37, 0x25, 0x77, 0x24, 0x26, 0x74, 0x6a, 0x28, 0x53, 0x4d, 0x69, 0x22, 0x5c, 0x44, 0x31,
	0x36, 0x58, 0x3b, 0x7a, 0x51, 0x5f, 0x52,
}

func passcode(s string) []byte {
	out := make([]byte, 0, len(s))
	for i := 0; i < len(s) && i < 16; i++ {
		p := int(s[i]) + i
		if p > 126 {
			p = 32 + p%127
		}
		out = append(out, passcodeSeq[p])
	}
	return out
}

// --- packet builders (offsets verified vs wfview structs + real captures) ---

func ctrlPacket(typ, seq uint16, sentid, rcvdid uint32) []byte {
	b := make([]byte, 16)
	le.PutUint32(b[0:], 0x10)
	le.PutUint16(b[4:], typ)
	le.PutUint16(b[6:], seq)
	le.PutUint32(b[8:], sentid)
	le.PutUint32(b[12:], rcvdid)
	return b
}

func buildLogin(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, user, pass, name string) []byte {
	b := make([]byte, 0x80)
	le.PutUint32(b[0:], 0x80)
	le.PutUint16(b[6:], seq)
	le.PutUint32(b[8:], sentid)
	le.PutUint32(b[12:], rcvdid)
	be.PutUint32(b[0x10:], 0x80-0x10)
	b[0x14] = 0x01 // requestreply
	b[0x15] = 0x00 // requesttype = login
	be.PutUint16(b[0x16:], innerSeq)
	le.PutUint16(b[0x1a:], tokReq)
	le.PutUint32(b[0x1c:], token)
	copy(b[0x40:0x50], passcode(user))
	copy(b[0x50:0x60], passcode(pass))
	nm := name
	if len(nm) > 16 {
		nm = nm[:16]
	}
	copy(b[0x60:0x70], []byte(nm))
	return b
}

func buildToken(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32) []byte {
	b := make([]byte, 0x40)
	le.PutUint32(b[0:], 0x40)
	le.PutUint16(b[6:], seq)
	le.PutUint32(b[8:], sentid)
	le.PutUint32(b[12:], rcvdid)
	be.PutUint32(b[0x10:], 0x40-0x10)
	b[0x14] = 0x01 // requestreply
	b[0x15] = 0x02 // requesttype = token
	be.PutUint16(b[0x16:], innerSeq)
	le.PutUint16(b[0x1a:], tokReq)
	le.PutUint32(b[0x1c:], token)
	return b
}

// buildConnInfo — 144-byte sendRequestStream on the CONTROL stream. Tells the
// rig to route the CI-V/audio streams to the authenticated session and which
// local ports we use. Values verified byte-for-byte vs a real Remote-Utility
// capture (civ4): requesttype=0x03, commoncap=0x8010, the rig's MAC echoed,
// name "IC-7610", scrambled username, rxenable=0 (audio off — CI-V only),
// rxcodec 0x10 / txcodec 0x04, rxsample 16000 / txsample 8000 (BE), civport /
// audioport (BE), txbuffer 100.
func buildConnInfo(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, user string, rigMAC []byte, civPort, audioPort uint16) []byte {
	b := make([]byte, 0x90)
	le.PutUint32(b[0:], 0x90)
	le.PutUint16(b[6:], seq)
	le.PutUint32(b[8:], sentid)
	le.PutUint32(b[12:], rcvdid)
	be.PutUint32(b[0x10:], 0x90-0x10)
	b[0x14] = 0x01 // requestreply
	b[0x15] = 0x03 // requesttype = conninfo / open streams
	be.PutUint16(b[0x16:], innerSeq)
	le.PutUint16(b[0x1a:], tokReq)
	le.PutUint32(b[0x1c:], token)
	le.PutUint16(b[0x27:], 0x8010) // commoncap
	copy(b[0x2a:0x30], rigMAC)     // macaddress (the rig's, echoed back)
	copy(b[0x40:0x60], []byte("IC-7610"))
	copy(b[0x60:0x70], passcode(user))
	b[0x70] = 0x00 // rxenable (0 = audio off)
	b[0x71] = 0x00 // txenable
	b[0x72] = 0x10 // rxcodec
	b[0x73] = 0x04 // txcodec
	be.PutUint32(b[0x74:], 16000) // rxsample
	be.PutUint32(b[0x78:], 8000)  // txsample
	be.PutUint32(b[0x7c:], uint32(civPort))
	be.PutUint32(b[0x80:], uint32(audioPort))
	be.PutUint32(b[0x84:], 100) // txbuffer
	b[0x88] = 0x00              // convert
	return b
}

// buildOpenClose — 22-byte start/stop for the CI-V stream. magic 0x04=open,
// 0x00=close. data=0x01c0 (@0x10), civSeq (BE @0x13), magic (@0x15).
func buildOpenClose(seq uint16, sentid, rcvdid uint32, civSeq uint16, magic byte) []byte {
	b := make([]byte, 0x16)
	le.PutUint32(b[0:], 0x16)
	le.PutUint16(b[6:], seq)
	le.PutUint32(b[8:], sentid)
	le.PutUint32(b[12:], rcvdid)
	le.PutUint16(b[0x10:], 0x01c0)
	be.PutUint16(b[0x13:], civSeq)
	b[0x15] = magic
	return b
}

// buildCivData — wraps raw CI-V bytes: 21-byte header (reply 0xc1 @0x10,
// datalen LE @0x11, civSeq BE @0x13) + CI-V frame @0x15.
func buildCivData(seq uint16, sentid, rcvdid uint32, civSeq uint16, civ []byte) []byte {
	n := 0x15 + len(civ)
	b := make([]byte, n)
	le.PutUint32(b[0:], uint32(n))
	le.PutUint16(b[6:], seq)
	le.PutUint32(b[8:], sentid)
	le.PutUint32(b[12:], rcvdid)
	b[0x10] = 0xc1
	le.PutUint16(b[0x11:], uint16(len(civ)))
	be.PutUint16(b[0x13:], civSeq)
	copy(b[0x15:], civ)
	return b
}

func header(b []byte) (length uint32, typ, seq uint16, sentid, rcvdid uint32, ok bool) {
	if len(b) < 16 {
		return 0, 0, 0, 0, 0, false
	}
	return le.Uint32(b[0:]), le.Uint16(b[4:]), le.Uint16(b[6:]), le.Uint32(b[8:]), le.Uint32(b[12:]), true
}

func localID(conn net.Conn) uint32 {
	a := conn.LocalAddr().(*net.UDPAddr)
	return uint32(a.IP.To4()[0])<<24 | uint32(a.IP.To4()[1])<<16 | uint32(uint16(a.Port))
}

func recv(conn net.Conn, ms int, buf []byte) ([]byte, bool) {
	_ = conn.SetReadDeadline(time.Now().Add(time.Duration(ms) * time.Millisecond))
	n, err := conn.Read(buf)
	if err != nil {
		return nil, false
	}
	return append([]byte(nil), buf[:n]...), true
}

func dump(tag string, p []byte) { fmt.Printf("%s (%d)\n%s\n", tag, len(p), hex.Dump(p)) }

// pingReply mirrors a ping, swaps ids, sets the reply flag at offset 16.
func pingReply(pkt []byte, myID, remoteID uint32) []byte {
	r := append([]byte(nil), pkt...)
	if len(r) >= 17 {
		le.PutUint32(r[8:], myID)
		le.PutUint32(r[12:], remoteID)
		r[16] = 0x01
	}
	return r
}

// handshake: areYouThere(seq0) → iAmHere → areYouReady(seq1) → iAmReady.
// Returns the rig's remote id. Replies to any pings meanwhile.
func handshake(conn net.Conn, myID uint32, label string) (uint32, bool) {
	buf := make([]byte, 2048)
	conn.Write(ctrlPacket(0x03, 0, myID, 0)) // areYouThere
	fmt.Printf("[%s] TX areYouThere\n", label)
	var remoteID uint32
	deadline := time.Now().Add(4 * time.Second)
	lastTry := time.Now()
	for time.Now().Before(deadline) {
		p, ok := recv(conn, 200, buf)
		if !ok {
			if remoteID == 0 && time.Since(lastTry) > 500*time.Millisecond {
				conn.Write(ctrlPacket(0x03, 0, myID, 0))
				lastTry = time.Now()
			}
			continue
		}
		_, typ, _, sentid, _, ok := header(p)
		if !ok {
			continue
		}
		switch typ {
		case 0x04: // iAmHere
			remoteID = sentid
			fmt.Printf("[%s] iAmHere remoteID=0x%08X → TX areYouReady\n", label, remoteID)
			conn.Write(ctrlPacket(0x06, 1, myID, remoteID))
		case 0x06: // iAmReady
			if remoteID != 0 {
				fmt.Printf("[%s] iAmReady — link up ✓\n", label)
				return remoteID, true
			}
		case 0x07: // ping
			conn.Write(pingReply(p, myID, remoteID))
		}
	}
	return remoteID, false
}

func main() {
	if len(os.Args) < 4 {
		fmt.Println("usage: icomnettest <rig-ip> <user> <pass> [compname]")
		os.Exit(2)
	}
	ip, user, pass := os.Args[1], os.Args[2], os.Args[3]
	compName := "OpsLog"
	if len(os.Args) >= 5 {
		compName = os.Args[4]
	}

	// ===================== CONTROL STREAM (50001) =====================
	ctrl, err := net.Dial("udp4", net.JoinHostPort(ip, "50001"))
	if err != nil {
		fmt.Printf("dial control: %v\n", err)
		os.Exit(1)
	}
	defer ctrl.Close()
	cID := localID(ctrl)
	fmt.Printf("=== CONTROL 50001 (myID=0x%08X) ===\n", cID)
	fmt.Printf("scrambled user=% X  pass=% X\n\n", passcode(user), passcode(pass))

	cRemote, ok := handshake(ctrl, cID, "ctrl")
	if !ok {
		fmt.Println("control handshake failed")
		return
	}

	// login → token
	var cTracked uint16 = 1
	var cInner uint16 = 1
	tokReq := uint16(0x0c77)
	dump("[ctrl] TX login", buildLogin(cTracked, cInner, tokReq, cID, cRemote, 0, user, pass, compName))
	ctrl.Write(buildLogin(cTracked, cInner, tokReq, cID, cRemote, 0, user, pass, compName))
	cTracked++
	cInner++

	var token uint32
	buf := make([]byte, 2048)
	deadline := time.Now().Add(4 * time.Second)
	for token == 0 && time.Now().Before(deadline) {
		p, ok := recv(ctrl, 200, buf)
		if !ok {
			continue
		}
		length, typ, _, _, _, _ := header(p)
		if typ == 0x00 && length == 0x60 && len(p) >= 0x34 { // login response
			token = le.Uint32(p[0x1c:])
			errCode := le.Uint32(p[0x30:])
			if errCode != 0 || token == 0 {
				fmt.Printf(">> LOGIN REJECTED err=0x%08X token=0x%08X\n", errCode, token)
				return
			}
			fmt.Printf(">> LOGIN OK ✓ token=0x%08X\n", token)
			ctrl.Write(buildToken(cTracked, cInner, tokReq, cID, cRemote, token))
			cTracked++
			cInner++
		} else if typ == 0x07 {
			ctrl.Write(pingReply(p, cID, cRemote))
		}
	}
	if token == 0 {
		fmt.Println("no token — login not accepted")
		return
	}

	// Send conninfo on the control stream — routes the CI-V stream to this
	// authenticated session and announces our civ/audio local ports (50002/3).
	rigMAC := []byte{0x00, 0x90, 0xc7, 0x09, 0xba, 0x3f} // F6BGC's IC-7610 (from the caps packet)
	dump("[ctrl] TX conninfo", buildConnInfo(cTracked, cInner, tokReq, cID, cRemote, token, user, rigMAC, 50002, 50003))
	ctrl.Write(buildConnInfo(cTracked, cInner, tokReq, cID, cRemote, token, user, rigMAC, 50002, 50003))
	cTracked++
	cInner++
	// Let the rig's caps/conninfo replies flow for ~600ms (reply to pings).
	drainEnd := time.Now().Add(600 * time.Millisecond)
	for time.Now().Before(drainEnd) {
		if p, ok := recv(ctrl, 100, buf); ok {
			if _, typ, _, _, _, _ := header(p); typ == 0x07 {
				ctrl.Write(pingReply(p, cID, cRemote))
			}
		}
	}

	// ===================== CI-V STREAM (50002) =====================
	// Bind our civ socket to LOCAL port 50002 (= the civport announced above),
	// as the Remote Utility does. Requires the Remote Utility to be CLOSED.
	civ, err := net.DialUDP("udp4", &net.UDPAddr{Port: 50002}, &net.UDPAddr{IP: net.ParseIP(ip), Port: 50002})
	if err != nil {
		fmt.Printf("dial civ (local :50002 — is the Remote Utility still running?): %v\n", err)
		return
	}
	defer civ.Close()
	vID := localID(civ)
	fmt.Printf("\n=== CI-V 50002 (myID=0x%08X) ===\n", vID)
	vRemote, ok := handshake(civ, vID, "civ")
	if !ok {
		fmt.Println("CI-V handshake failed (may need the conninfo packet on control first)")
		return
	}

	var vTracked uint16 = 1 // outer tracked seq @0x06
	var vCivSeq uint16 = 1  // inner CI-V seq @0x13 (BE)
	// openClose(open) starts CI-V data flow.
	dump("[civ] TX openClose(open)", buildOpenClose(vTracked, vID, vRemote, vCivSeq, 0x04))
	civ.Write(buildOpenClose(vTracked, vID, vRemote, vCivSeq, 0x04))
	vTracked++
	vCivSeq++

	// Try several read commands, spaced out. Some rigs NG the basic 0x03 read
	// over the network tunnel; 0x25 / 0x04 and unsolicited transceive frames
	// (sent when you turn the VFO) still work. The tunnel itself is proven, so
	// this figures out which read the rig actually answers.
	sendCiv := func(name string, f []byte) {
		fmt.Printf("[civ] TX %s\n", name)
		civ.Write(buildCivData(vTracked, vID, vRemote, vCivSeq, f))
		vTracked++
		vCivSeq++
	}
	// The rig is in STANDBY (network up, radio off) — it NG's every command
	// until powered on via CI-V. Send power-on (0x18 0x01, with an FE wake
	// preamble, as the Remote Utility does), then poll read-freq while it boots.
	powerOn := make([]byte, 0, 32)
	for i := 0; i < 25; i++ {
		powerOn = append(powerOn, 0xFE)
	}
	powerOn = append(powerOn, 0xFE, 0xFE, 0x98, 0xE0, 0x18, 0x01, 0xFD)
	time.Sleep(300 * time.Millisecond)
	sendCiv("POWER ON (0x18 01)", powerOn)
	fmt.Print("\n>>> rig booting (~10-15 s) — polling read-freq until it answers <<<\n\n")
	readFreq := []byte{0xFE, 0xFE, 0x98, 0xE0, 0x03, 0xFD}

	cbuf := make([]byte, 4096)
	vbuf := make([]byte, 4096)
	end := time.Now().Add(30 * time.Second)
	lastIdleC, lastIdleV, lastCmd := time.Now(), time.Now(), time.Now()
	for time.Now().Before(end) {
		if p, ok := recv(ctrl, 40, cbuf); ok {
			if _, typ, _, _, _, _ := header(p); typ == 0x07 {
				ctrl.Write(pingReply(p, cID, cRemote))
			}
		} else if time.Since(lastIdleC) > 200*time.Millisecond {
			ctrl.Write(ctrlPacket(0x00, 0, cID, cRemote))
			lastIdleC = time.Now()
		}
		if p, ok := recv(civ, 40, vbuf); ok {
			_, typ, _, _, _, _ := header(p)
			if typ == 0x07 {
				civ.Write(pingReply(p, vID, vRemote))
			} else if typ == 0x00 && len(p) > 0x15 && p[0x10] == 0xc1 {
				f := p[0x15:]
				if d := decodeCiv(f); d != "" {
					fmt.Printf(">> CI-V RX: % X   %s\n", f, d)
				}
			}
		} else if time.Since(lastIdleV) > 200*time.Millisecond {
			civ.Write(ctrlPacket(0x00, 0, vID, vRemote))
			lastIdleV = time.Now()
		}
		if time.Since(lastCmd) > 1000*time.Millisecond {
			sendCiv("read-freq 0x03", readFreq)
			lastCmd = time.Now()
		}
	}

	// Clean close.
	civ.Write(buildOpenClose(vTracked, vID, vRemote, vCivSeq, 0x00)) // openClose(close)
	civ.Write(ctrlPacket(0x05, 0, vID, vRemote))                     // disconnect
	ctrl.Write(ctrlPacket(0x05, 0, cID, cRemote))
	fmt.Println("\nDone. Look for '>> CI-V RX:' and 'FREQUENCY reply'.")
}

// decodeCiv describes a received CI-V frame (FE FE <to> <from> <cmd> … FD).
// Only frames FROM the rig (from=0x98) are interesting; our own echoed commands
// (from=0xE0) return "" so they're not printed.
func decodeCiv(f []byte) string {
	if len(f) < 6 || f[0] != 0xFE || f[1] != 0xFE {
		return ""
	}
	if f[3] != 0x98 { // not from the rig (our echoed command) — skip
		return ""
	}
	cmd := f[4]
	body := f[5 : len(f)-1] // between cmd and the trailing FD
	switch cmd {
	case 0xFA:
		return "NG (command rejected)"
	case 0xFB:
		return "OK (ack)"
	case 0x00, 0x03, 0x05: // (transceive) freq / read-freq
		if len(body) >= 5 {
			return "FREQ " + decodeFreq(body[:5])
		}
	case 0x25: // read/set VFO freq (body = subcmd + 5 BCD)
		if len(body) >= 6 {
			return fmt.Sprintf("VFO%d FREQ %s", body[0], decodeFreq(body[1:6]))
		}
	case 0x01, 0x04: // (transceive) mode / read-mode
		if len(body) >= 1 {
			return fmt.Sprintf("MODE 0x%02X filt 0x%02X", body[0], lastOr(body, 1))
		}
	}
	return fmt.Sprintf("cmd 0x%02X", cmd)
}

func lastOr(b []byte, i int) byte {
	if i < len(b) {
		return b[i]
	}
	return 0
}

// decodeFreq turns Icom little-endian BCD (5 bytes) into a MHz string.
func decodeFreq(bcd []byte) string {
	var hz uint64
	mul := uint64(1)
	for _, b := range bcd {
		hz += uint64(b&0x0f) * mul
		mul *= 10
		hz += uint64(b>>4) * mul
		mul *= 10
	}
	return fmt.Sprintf("%.6f MHz", float64(hz)/1e6)
}
