package client

import (
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"io"
	"net"
	"sync"
	"time"

	"github.com/duffy/usb-server/internal/protocol"
)

// Direct tunnel wire format.
//
// Handshake, sent by the connecting (use) side:
//
//	[4] magic "USBD"  [1] version  [16] tunnel ID  [32] peer token
//
// Reply, sent by the listening (share) side:
//
//	[4] magic "USBD"  [1] version  [1] status (0 = accepted)
//
// Everything after that is length-prefixed encrypted frames:
//
//	[4] length (big endian)  [length bytes] sealed frame
const (
	directMagic   = "USBD"
	directVersion = 1

	handshakeSize      = 4 + 1 + protocol.TunnelHeaderSize + 32
	handshakeReplySize = 4 + 1 + 1

	// directHandshakeTimeout bounds the handshake. A peer that reaches the
	// port but does not speak this protocol must not hold the slot.
	directHandshakeTimeout = 5 * time.Second

	// directDialTimeout bounds one connection attempt. Candidate addresses
	// are tried in parallel, so this is also how long the whole attempt takes
	// before falling back to the relay.
	directDialTimeout = 3 * time.Second

	// maxDirectFrame caps a single frame, so a hostile or corrupt length
	// prefix cannot make us allocate arbitrarily.
	maxDirectFrame = 2 << 20
)

// Handshake status codes.
const (
	directAccepted    = 0
	directUnknownTun  = 1
	directBadToken    = 2
	directWrongVerson = 3
)

// directConn carries length-prefixed frames over a plain TCP connection.
//
// It deliberately does no encryption of its own: tunnel frames are sealed one
// level up, by the tunnel's codec, so that relayed and direct tunnels get the
// same protection. Putting it here instead would leave the relay path in
// cleartext — the one path where a third party is actually in the middle.
type directConn struct {
	conn net.Conn

	writeMu sync.Mutex
}

func newDirectConn(conn net.Conn) *directConn {
	return &directConn{conn: conn}
}

// WriteFrame sends one length-prefixed frame.
func (d *directConn) WriteFrame(payload []byte) error {
	if len(payload) > maxDirectFrame {
		return fmt.Errorf("frame of %d bytes exceeds the %d byte limit", len(payload), maxDirectFrame)
	}

	buf := make([]byte, 4+len(payload))
	binary.BigEndian.PutUint32(buf, uint32(len(payload)))
	copy(buf[4:], payload)

	// TCP writes from several goroutines would interleave and corrupt the
	// framing, so sends are serialised.
	d.writeMu.Lock()
	defer d.writeMu.Unlock()

	if _, err := d.conn.Write(buf); err != nil {
		return fmt.Errorf("writing frame: %w", err)
	}
	return nil
}

// ReadFrame reads one length-prefixed frame.
func (d *directConn) ReadFrame() ([]byte, error) {
	var lenBuf [4]byte
	if _, err := io.ReadFull(d.conn, lenBuf[:]); err != nil {
		return nil, err
	}

	length := binary.BigEndian.Uint32(lenBuf[:])
	if length == 0 || length > maxDirectFrame {
		return nil, fmt.Errorf("frame length %d is out of range", length)
	}

	payload := make([]byte, length)
	if _, err := io.ReadFull(d.conn, payload); err != nil {
		return nil, err
	}
	return payload, nil
}

// RemoteAddr reports the peer address, for logging.
func (d *directConn) RemoteAddr() string { return d.conn.RemoteAddr().String() }

// Close closes the underlying connection.
func (d *directConn) Close() error { return d.conn.Close() }

// buildHandshake assembles the greeting the connecting side sends.
func buildHandshake(tunnelID, peerToken string) ([]byte, error) {
	tokenBytes, err := hex.DecodeString(peerToken)
	if err != nil || len(tokenBytes) != 32 {
		return nil, fmt.Errorf("invalid peer token")
	}
	if len(tunnelID) != protocol.TunnelHeaderSize {
		return nil, fmt.Errorf("tunnel ID is %d bytes, want %d", len(tunnelID), protocol.TunnelHeaderSize)
	}

	buf := make([]byte, 0, handshakeSize)
	buf = append(buf, directMagic...)
	buf = append(buf, directVersion)
	buf = append(buf, tunnelID...)
	buf = append(buf, tokenBytes...)
	return buf, nil
}

// parseHandshake validates the greeting and returns the requested tunnel ID
// and the presented token in hex form.
func parseHandshake(data []byte) (tunnelID, peerToken string, err error) {
	if len(data) != handshakeSize {
		return "", "", fmt.Errorf("handshake is %d bytes, want %d", len(data), handshakeSize)
	}
	if string(data[:4]) != directMagic {
		return "", "", fmt.Errorf("bad magic")
	}
	if data[4] != directVersion {
		return "", "", fmt.Errorf("unsupported version %d", data[4])
	}

	tunnelID = string(data[5 : 5+protocol.TunnelHeaderSize])
	peerToken = hex.EncodeToString(data[5+protocol.TunnelHeaderSize:])
	return tunnelID, peerToken, nil
}

func buildHandshakeReply(status byte) []byte {
	buf := make([]byte, 0, handshakeReplySize)
	buf = append(buf, directMagic...)
	buf = append(buf, directVersion)
	buf = append(buf, status)
	return buf
}

func parseHandshakeReply(data []byte) error {
	if len(data) != handshakeReplySize {
		return fmt.Errorf("reply is %d bytes, want %d", len(data), handshakeReplySize)
	}
	if string(data[:4]) != directMagic {
		return fmt.Errorf("bad magic in reply")
	}
	if data[4] != directVersion {
		return fmt.Errorf("peer speaks version %d, we speak %d", data[4], directVersion)
	}

	switch data[5] {
	case directAccepted:
		return nil
	case directUnknownTun:
		return fmt.Errorf("peer does not know this tunnel")
	case directBadToken:
		return fmt.Errorf("peer rejected our token")
	case directWrongVerson:
		return fmt.Errorf("peer rejected our version")
	default:
		return fmt.Errorf("peer rejected the connection (status %d)", data[5])
	}
}

// localEndpoints lists host:port addresses on this machine's own interfaces.
//
// Loopback is skipped — a peer on another machine cannot use it — but every
// other usable unicast address is offered, because which one is reachable
// depends on the network and only the attempt can tell.
func localEndpoints(port int) []string {
	if port == 0 {
		return nil
	}

	addrs, err := net.InterfaceAddrs()
	if err != nil {
		return nil
	}

	var endpoints []string
	for _, addr := range addrs {
		ipNet, ok := addr.(*net.IPNet)
		if !ok {
			continue
		}
		ip := ipNet.IP
		if ip.IsLoopback() || ip.IsUnspecified() || !ip.IsGlobalUnicast() {
			continue
		}
		// Link-local IPv6 needs a zone to be dialable and rarely helps here.
		if ip.To4() == nil && ip.IsLinkLocalUnicast() {
			continue
		}
		endpoints = append(endpoints, net.JoinHostPort(ip.String(), fmt.Sprint(port)))
	}
	return endpoints
}

// dialDirect races the candidate addresses and returns the first connection
// that completes the handshake.
//
// Racing rather than trying in sequence matters: an unreachable address on a
// different subnet typically does not refuse the connection, it hangs until
// the timeout, and trying those one after another would take longer than the
// relay fallback it is meant to avoid.
func dialDirect(endpoints []string, tunnelID, peerToken string) (*directConn, string, error) {
	if len(endpoints) == 0 {
		return nil, "", fmt.Errorf("no candidate addresses")
	}

	greeting, err := buildHandshake(tunnelID, peerToken)
	if err != nil {
		return nil, "", err
	}

	type result struct {
		conn *directConn
		addr string
		err  error
	}
	results := make(chan result, len(endpoints))

	for _, endpoint := range endpoints {
		go func(addr string) {
			conn, err := attemptDirect(addr, greeting)
			results <- result{conn: conn, addr: addr, err: err}
		}(endpoint)
	}

	var lastErr error
	var winner *directConn
	var winnerAddr string

	// Collect every result so that a connection completing after we already
	// have a winner still gets closed instead of leaking.
	for range endpoints {
		r := <-results
		switch {
		case r.err != nil:
			lastErr = r.err
		case winner == nil:
			winner, winnerAddr = r.conn, r.addr
		default:
			r.conn.Close()
		}
	}

	if winner == nil {
		return nil, "", fmt.Errorf("no address reachable: %w", lastErr)
	}
	return winner, winnerAddr, nil
}

// attemptDirect performs one dial plus handshake.
func attemptDirect(addr string, greeting []byte) (*directConn, error) {
	conn, err := net.DialTimeout("tcp", addr, directDialTimeout)
	if err != nil {
		return nil, err
	}

	conn.SetDeadline(time.Now().Add(directHandshakeTimeout))

	if _, err := conn.Write(greeting); err != nil {
		conn.Close()
		return nil, fmt.Errorf("sending handshake to %s: %w", addr, err)
	}

	reply := make([]byte, handshakeReplySize)
	if _, err := io.ReadFull(conn, reply); err != nil {
		conn.Close()
		return nil, fmt.Errorf("reading handshake reply from %s: %w", addr, err)
	}
	if err := parseHandshakeReply(reply); err != nil {
		conn.Close()
		return nil, fmt.Errorf("handshake with %s: %w", addr, err)
	}

	// Clear the handshake deadline; tunnel traffic has no fixed timing.
	conn.SetDeadline(time.Time{})

	return newDirectConn(conn), nil
}
