package measurexlite

//
// Conn tracing
//

import (
	"net"
	"time"

	"github.com/ooni/probe-cli/v3/internal/model"
	"github.com/ooni/probe-cli/v3/internal/netxlite"
	"github.com/ooni/probe-cli/v3/internal/tracex"
)

// MaybeClose is a convenience function for closing a conn only when such a conn isn't nil.
func MaybeClose(conn net.Conn) (err error) {
	if conn != nil {
		err = conn.Close()
	}
	return
}

// MaybeWrapNetConn implements model.Trace.MaybeWrapNetConn.
func (tx *Trace) MaybeWrapNetConn(conn net.Conn) net.Conn {
	return &connTrace{
		Conn: conn,
		tx:   tx,
	}
}

// connTrace is a trace-aware net.Conn.
type connTrace struct {
	// Implementation note: it seems safe to use embedding here because net.Conn
	// is an interface from the standard library that we don't control
	net.Conn
	tx *Trace
}

var _ net.Conn = &connTrace{}

// Read implements net.Conn.Read and saves network events.
func (c *connTrace) Read(b []byte) (int, error) {
	network := c.RemoteAddr().Network()
	addr := c.RemoteAddr().String()
	started := c.tx.TimeSince(c.tx.ZeroTime)
	count, err := c.Conn.Read(b)
	finished := c.tx.TimeSince(c.tx.ZeroTime)
	select {
	case c.tx.networkEvent <- NewArchivalNetworkEvent(
		c.tx.Index, started, netxlite.ReadOperation, network, addr, count, err, finished):
	default: // buffer is full
	}
	return count, err
}

// Write implements net.Conn.Write and saves network events.
func (c *connTrace) Write(b []byte) (int, error) {
	network := c.RemoteAddr().Network()
	addr := c.RemoteAddr().String()
	started := c.tx.TimeSince(c.tx.ZeroTime)
	count, err := c.Conn.Write(b)
	finished := c.tx.TimeSince(c.tx.ZeroTime)
	select {
	case c.tx.networkEvent <- NewArchivalNetworkEvent(
		c.tx.Index, started, netxlite.WriteOperation, network, addr, count, err, finished):
	default: // buffer is full
	}
	return count, err
}

// MaybeUDPLikeClose is a convenience function for closing a conn only when such a conn isn't nil.
func MaybeCloseUDPLikeConn(conn model.UDPLikeConn) (err error) {
	if conn != nil {
		err = conn.Close()
	}
	return
}

// MaybeWrapUDPLikeConn implements model.Trace.MaybeWrapUDPLikeConn.
func (tx *Trace) MaybeWrapUDPLikeConn(conn model.UDPLikeConn) model.UDPLikeConn {
	return &udpLikeConnTrace{
		UDPLikeConn: conn,
		tx:          tx,
	}
}

// udpLikeConnTrace is a trace-aware model.UDPLikeConn.
type udpLikeConnTrace struct {
	// Implementation note: it seems ~safe to use embedding here because model.UDPLikeConn
	// contains fields deriving from how lucas-clemente/quic-go uses the standard library
	model.UDPLikeConn
	tx *Trace
}

// Read implements model.UDPLikeConn.ReadFrom and saves network events.
func (c *udpLikeConnTrace) ReadFrom(b []byte) (int, net.Addr, error) {
	started := c.tx.TimeSince(c.tx.ZeroTime)
	count, addr, err := c.UDPLikeConn.ReadFrom(b)
	finished := c.tx.TimeSince(c.tx.ZeroTime)
	address := addrStringIfNotNil(addr)
	select {
	case c.tx.networkEvent <- NewArchivalNetworkEvent(
		c.tx.Index, started, netxlite.ReadFromOperation, "udp", address, count, err, finished):
	default: // buffer is full
	}
	return count, addr, err
}

// Write implements model.UDPLikeConn.WriteTo and saves network events.
func (c *udpLikeConnTrace) WriteTo(b []byte, addr net.Addr) (int, error) {
	started := c.tx.TimeSince(c.tx.ZeroTime)
	address := addr.String()
	count, err := c.UDPLikeConn.WriteTo(b, addr)
	finished := c.tx.TimeSince(c.tx.ZeroTime)
	select {
	case c.tx.networkEvent <- NewArchivalNetworkEvent(
		c.tx.Index, started, netxlite.WriteToOperation, "udp", address, count, err, finished):
	default: // buffer is full
	}
	return count, err
}

// addrStringIfNotNil returns the string of the given addr
// unless the addr is nil, in which case it returns an empty string.
func addrStringIfNotNil(addr net.Addr) (out string) {
	if addr != nil {
		out = addr.String()
	}
	return
}

// NewArchivalNetworkEvent creates a new model.ArchivalNetworkEvent.
func NewArchivalNetworkEvent(index int64, started time.Duration, operation string, network string,
	address string, count int, err error, finished time.Duration) *model.ArchivalNetworkEvent {
	return &model.ArchivalNetworkEvent{
		Address:       address,
		Failure:       tracex.NewFailure(err),
		NumBytes:      int64(count),
		Operation:     operation,
		Proto:         network,
		T0:            started.Seconds(),
		T:             finished.Seconds(),
		TransactionID: index,
		Tags:          []string{},
	}
}

// NewAnnotationArchivalNetworkEvent is a simplified NewArchivalNetworkEvent
// where we create a simple annotation without attached I/O info.
func NewAnnotationArchivalNetworkEvent(
	index int64, time time.Duration, operation string) *model.ArchivalNetworkEvent {
	return NewArchivalNetworkEvent(index, time, operation, "", "", 0, nil, time)
}

// NetworkEvents drains the network events buffered inside the NetworkEvent channel.
func (tx *Trace) NetworkEvents() (out []*model.ArchivalNetworkEvent) {
	for {
		select {
		case ev := <-tx.networkEvent:
			out = append(out, ev)
		default:
			return // done
		}
	}
}

// FirstNetworkEventOrNil drains the network events buffered inside the NetworkEvents channel
// and returns the first NetworkEvent, if any. Otherwise, it returns nil.
func (tx *Trace) FirstNetworkEventOrNil() *model.ArchivalNetworkEvent {
	ev := tx.NetworkEvents()
	if len(ev) < 1 {
		return nil
	}
	return ev[0]
}
