package measurex

//
// QUIC
//
// Wrappers for QUIC to store events into a WritableDB.
//

import (
	"context"
	"crypto/tls"
	"net"
	"time"

	"github.com/lucas-clemente/quic-go"
	"github.com/ooni/probe-cli/v3/internal/model"
	"github.com/ooni/probe-cli/v3/internal/netxlite"
)

type quicListenerDB struct {
	model.QUICListener
	begin time.Time
	db    WritableDB
}

func (ql *quicListenerDB) Listen(addr *net.UDPAddr) (model.UDPLikeConn, error) {
	pconn, err := ql.QUICListener.Listen(addr)
	if err != nil {
		return nil, err
	}
	return &udpLikeConnDB{
		UDPLikeConn: pconn,
		begin:       ql.begin,
		db:          ql.db,
	}, nil
}

type udpLikeConnDB struct {
	model.UDPLikeConn
	begin time.Time
	db    WritableDB
}

func (c *udpLikeConnDB) WriteTo(p []byte, addr net.Addr) (int, error) {
	started := time.Since(c.begin).Seconds()
	count, err := c.UDPLikeConn.WriteTo(p, addr)
	finished := time.Since(c.begin).Seconds()
	c.db.InsertIntoReadWrite(&NetworkEvent{
		Operation:  "write_to",
		Network:    "udp",
		RemoteAddr: addr.String(),
		Started:    started,
		Finished:   finished,
		Failure:    NewFailure(err),
		Count:      count,
	})
	return count, err
}

func (c *udpLikeConnDB) ReadFrom(b []byte) (int, net.Addr, error) {
	started := time.Since(c.begin).Seconds()
	count, addr, err := c.UDPLikeConn.ReadFrom(b)
	finished := time.Since(c.begin).Seconds()
	c.db.InsertIntoReadWrite(&NetworkEvent{
		Operation:  "read_from",
		Network:    "udp",
		RemoteAddr: addrStringIfNotNil(addr),
		Started:    started,
		Finished:   finished,
		Failure:    NewFailure(err),
		Count:      count,
	})
	return count, addr, err
}

func (c *udpLikeConnDB) Close() error {
	started := time.Since(c.begin).Seconds()
	err := c.UDPLikeConn.Close()
	finished := time.Since(c.begin).Seconds()
	c.db.InsertIntoClose(&NetworkEvent{
		Operation:  "close",
		Network:    "udp",
		RemoteAddr: "",
		Started:    started,
		Finished:   finished,
		Failure:    NewFailure(err),
		Count:      0,
	})
	return err
}

// NewQUICDialerWithoutResolver creates a new QUICDialer that is not
// attached to any resolver. This means that every attempt to dial any
// address containing a domain name will fail. This QUICDialer will
// save any event into the WritableDB. Any QUICConn created by it will
// likewise save any event into the WritableDB.
func (mx *Measurer) NewQUICDialerWithoutResolver(db WritableDB, logger model.Logger) model.QUICDialer {
	return &quicDialerDB{db: db, logger: logger, begin: mx.Begin}
}

type quicDialerDB struct {
	model.QUICDialer
	begin  time.Time
	db     WritableDB
	logger model.Logger
}

func (qh *quicDialerDB) DialContext(ctx context.Context, address string,
	tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
	started := time.Since(qh.begin).Seconds()
	var state tls.ConnectionState
	listener := &quicListenerDB{
		QUICListener: netxlite.NewQUICListener(),
		begin:        qh.begin,
		db:           qh.db,
	}
	dialer := netxlite.NewQUICDialerWithoutResolver(listener, qh.logger)
	defer dialer.CloseIdleConnections()
	sess, err := dialer.DialContext(ctx, address, tlsConfig, quicConfig)
	if err == nil {
		<-sess.HandshakeComplete().Done() // robustness (the dialer already does that)
		state = sess.ConnectionState().TLS.ConnectionState
	}
	finished := time.Since(qh.begin).Seconds()
	qh.db.InsertIntoQUICHandshake(&QUICTLSHandshakeEvent{
		Network:         "udp",
		RemoteAddr:      address,
		SNI:             tlsConfig.ServerName,
		ALPN:            tlsConfig.NextProtos,
		SkipVerify:      tlsConfig.InsecureSkipVerify,
		Started:         started,
		Finished:        finished,
		Failure:         NewFailure(err),
		Oddity:          qh.computeOddity(err),
		TLSVersion:      netxlite.TLSVersionString(state.Version),
		CipherSuite:     netxlite.TLSCipherSuiteString(state.CipherSuite),
		NegotiatedProto: state.NegotiatedProtocol,
		PeerCerts:       peerCerts(nil, &state),
	})
	return sess, err
}

func (qh *quicDialerDB) computeOddity(err error) Oddity {
	if err == nil {
		return ""
	}
	switch err.Error() {
	case netxlite.FailureGenericTimeoutError:
		return OddityQUICHandshakeTimeout
	case netxlite.FailureHostUnreachable:
		return OddityQUICHandshakeHostUnreachable
	default:
		return OddityQUICHandshakeOther
	}
}

func (qh *quicDialerDB) CloseIdleConnections() {
	// nothing to do
}
