package tracex

//
// TCP and connected UDP sockets
//

import (
	"context"
	"net"
	"time"

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

// DialerSaver saves events occurring during the dial
type DialerSaver struct {
	// Dialer is the underlying dialer,
	Dialer model.Dialer

	// Saver saves events.
	Saver *Saver
}

// NewConnectObserver returns a DialerWrapper that observes the
// connect event. This function will return nil, which is a valid
// DialerWrapper for netxlite.WrapDialer, if Saver is nil.
func (s *Saver) NewConnectObserver() model.DialerWrapper {
	if s == nil {
		return nil // valid DialerWrapper according to netxlite's docs
	}
	return &dialerConnectObserver{
		saver: s,
	}
}

type dialerConnectObserver struct {
	saver *Saver
}

var _ model.DialerWrapper = &dialerConnectObserver{}

func (w *dialerConnectObserver) WrapDialer(d model.Dialer) model.Dialer {
	return &DialerSaver{
		Dialer: d,
		Saver:  w.saver,
	}
}

// DialContext implements Dialer.DialContext
func (d *DialerSaver) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	start := time.Now()
	conn, err := d.Dialer.DialContext(ctx, network, address)
	stop := time.Now()
	d.Saver.Write(&EventConnectOperation{&EventValue{
		Address:  address,
		Duration: stop.Sub(start),
		Err:      NewFailureStr(err),
		Proto:    network,
		Time:     stop,
	}})
	return conn, err
}

func (d *DialerSaver) CloseIdleConnections() {
	d.Dialer.CloseIdleConnections()
}

// DialerConnSaver wraps the returned connection such that we
// collect all the read/write events that occur.
type DialerConnSaver struct {
	// Dialer is the underlying dialer
	Dialer model.Dialer

	// Saver saves events
	Saver *Saver
}

// NewReadWriteObserver returns a DialerWrapper that observes the
// I/O events. This function will return nil, which is a valid
// DialerWrapper for netxlite.WrapDialer, if Saver is nil.
func (s *Saver) NewReadWriteObserver() model.DialerWrapper {
	if s == nil {
		return nil // valid DialerWrapper according to netxlite's docs
	}
	return &dialerReadWriteObserver{
		saver: s,
	}
}

type dialerReadWriteObserver struct {
	saver *Saver
}

var _ model.DialerWrapper = &dialerReadWriteObserver{}

func (w *dialerReadWriteObserver) WrapDialer(d model.Dialer) model.Dialer {
	return &DialerConnSaver{
		Dialer: d,
		Saver:  w.saver,
	}
}

// DialContext implements Dialer.DialContext
func (d *DialerConnSaver) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	conn, err := d.Dialer.DialContext(ctx, network, address)
	if err != nil {
		return nil, err
	}
	return &dialerConnWrapper{saver: d.Saver, Conn: conn}, nil
}

func (d *DialerConnSaver) CloseIdleConnections() {
	d.Dialer.CloseIdleConnections()
}

type dialerConnWrapper struct {
	net.Conn
	saver *Saver
}

func (c *dialerConnWrapper) Read(p []byte) (int, error) {
	proto := c.Conn.RemoteAddr().Network()
	remoteAddr := c.Conn.RemoteAddr().String()
	start := time.Now()
	count, err := c.Conn.Read(p)
	stop := time.Now()
	c.saver.Write(&EventReadOperation{&EventValue{
		Address:  remoteAddr,
		Data:     p[:count],
		Duration: stop.Sub(start),
		Err:      NewFailureStr(err),
		NumBytes: count,
		Proto:    proto,
		Time:     stop,
	}})
	return count, err
}

func (c *dialerConnWrapper) Write(p []byte) (int, error) {
	proto := c.Conn.RemoteAddr().Network()
	remoteAddr := c.Conn.RemoteAddr().String()
	start := time.Now()
	count, err := c.Conn.Write(p)
	stop := time.Now()
	c.saver.Write(&EventWriteOperation{&EventValue{
		Address:  remoteAddr,
		Data:     p[:count],
		Duration: stop.Sub(start),
		Err:      NewFailureStr(err),
		NumBytes: count,
		Proto:    proto,
		Time:     stop,
	}})
	return count, err
}

var _ model.Dialer = &DialerSaver{}
var _ model.Dialer = &DialerConnSaver{}
var _ net.Conn = &dialerConnWrapper{}
