package measurex

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"time"

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

//
// API for reducing boilerplate for simple measurements.
//

// EasyHTTPRoundTripGET performs a GET with the given URL
// and default headers. This function will perform just
// a single HTTP round trip (i.e., no redirections).
//
// Arguments:
//
// - ctx is the context for deadline/timeout/cancellation;
//
// - timeout is the timeout for the whole operation;
//
// - URL is the URL to GET;
//
// Returns:
//
// - meas is a JSON serializable OONI measurement (this
// field will never be a nil pointer);
//
// - failure is either nil or a pointer to a OONI failure.
func (mx *Measurer) EasyHTTPRoundTripGET(ctx context.Context, timeout time.Duration,
	URL string) (meas *ArchivalMeasurement, failure *string) {
	ctx, cancel := context.WithTimeout(ctx, timeout) // honour the timeout
	defer cancel()
	db := &MeasurementDB{}
	req, err := NewHTTPRequestWithContext(ctx, "GET", URL, nil)
	if err != nil {
		failure := err.Error()
		return NewArchivalMeasurement(db.AsMeasurement()), &failure
	}
	txp := mx.NewTracingHTTPTransportWithDefaultSettings(db)
	resp, err := txp.RoundTrip(req)
	if err != nil {
		failure := err.Error()
		return NewArchivalMeasurement(db.AsMeasurement()), &failure
	}
	resp.Body.Close()
	return NewArchivalMeasurement(db.AsMeasurement()), nil
}

// EasyTLSConfig helps you to generate a *tls.Config.
type EasyTLSConfig struct {
	config *tls.Config
}

// NewEasyTLSConfig creates a new EasyTLSConfig instance.
func NewEasyTLSConfig() *EasyTLSConfig {
	return &EasyTLSConfig{
		config: &tls.Config{
			RootCAs: netxlite.NewDefaultCertPool(),
		},
	}
}

// NewEasyTLSConfigWithServerName creates a new EasyTLSConfig
// with an already configured value for ServerName.
func NewEasyTLSConfigWithServerName(serverName string) *EasyTLSConfig {
	return NewEasyTLSConfig().ServerName(serverName)
}

// ServerName sets the SNI value.
func (easy *EasyTLSConfig) ServerName(v string) *EasyTLSConfig {
	easy.config.ServerName = v
	return easy
}

// InsecureSkipVerify disables TLS verification.
func (easy *EasyTLSConfig) InsecureSkipVerify(v bool) *EasyTLSConfig {
	easy.config.InsecureSkipVerify = v
	return easy
}

// RootCAs allows the set the CA pool.
func (easy *EasyTLSConfig) RootCAs(v *x509.CertPool) *EasyTLSConfig {
	easy.config.RootCAs = v
	return easy
}

// asTLSConfig converts an *EasyTLSConfig to a *tls.Config.
func (easy *EasyTLSConfig) asTLSConfig() *tls.Config {
	if easy == nil || easy.config == nil {
		return &tls.Config{}
	}
	return easy.config
}

// EasyTLSConnectAndHandshake performs a TCP connect to a TCP endpoint
// followed by a TLS handshake using the given config.
//
// Arguments:
//
// - ctx is the context for deadline/timeout/cancellation;
//
// - endpoint is the TCP endpoint to connect to (e.g.,
// 8.8.8.8:443 where the address part of the endpoint MUST
// be an IPv4 or IPv6 address and MUST NOT be a domain);
//
// - tlsConfig is the EasyTLSConfig to use (MUST NOT be nil).
//
// Returns:
//
// - meas is a JSON serializable OONI measurement (this
// field will never be a nil pointer);
//
// - failure is either nil or a pointer to a OONI failure.
//
// Note:
//
// - we use the Measurer's TCPConnectTimeout and TLSHandshakeTimeout.
func (mx *Measurer) EasyTLSConnectAndHandshake(ctx context.Context, endpoint string,
	tlsConfig *EasyTLSConfig) (meas *ArchivalMeasurement, failure *string) {
	// Note: TLSConnectAndHandshakeWithDB uses the timeout configured inside mx.
	db := &MeasurementDB{}
	conn, err := mx.TLSConnectAndHandshakeWithDB(ctx, db, endpoint, tlsConfig.asTLSConfig())
	if err != nil {
		failure := err.Error()
		return NewArchivalMeasurement(db.AsMeasurement()), &failure
	}
	conn.Close()
	return NewArchivalMeasurement(db.AsMeasurement()), nil
}

// EasyTCPConnect performs a TCP connect to a TCP endpoint.
//
// Arguments:
//
// - ctx is the context for deadline/timeout/cancellation;
//
// - endpoint is the TCP endpoint to connect to (e.g.,
// 8.8.8.8:443 where the address part of the endpoint MUST
// be an IPv4 or IPv6 address and MUST NOT be a domain).
//
// Returns:
//
// - meas is a JSON serializable OONI measurement (this
// field will never be a nil pointer);
//
// - failure is either nil or a pointer to a OONI failure.
//
// Note:
//
// - we use the Measurer's TCPConnectTimeout.
func (mx *Measurer) EasyTCPConnect(ctx context.Context,
	endpoint string) (meas *ArchivalMeasurement, failure *string) {
	// Note: TCPConnectWithDB uses the timeout configured inside mx.
	db := &MeasurementDB{}
	conn, err := mx.TCPConnectWithDB(ctx, db, endpoint)
	if err != nil {
		failure := err.Error()
		return NewArchivalMeasurement(db.AsMeasurement()), &failure
	}
	conn.Close()
	return NewArchivalMeasurement(db.AsMeasurement()), nil
}

// easyOBFS4Params contains params for OBFS4.
type easyOBFS4Params struct {
	// Cert contains the MANDATORY certificate parameter.
	Cert string

	// DataDir is the MANDATORY directory where to store obfs4 data.
	DataDir string

	// Fingerprint is the MANDATORY bridge fingerprint.
	Fingerprint string

	// IATMode contains the MANDATORY iat-mode parameter.
	IATMode string
}

// newEasyOBFS4Params constructs an EasyOBFS4Params structure
// from the map[string][]string returned by the OONI API.
//
// This function will only fail when the rawParams contains
// more than one entry for each input key.
func newEasyOBFS4Params(dataDir string, rawParams map[string][]string) (*easyOBFS4Params, error) {
	out := &easyOBFS4Params{DataDir: dataDir}
	for key, values := range rawParams {
		var field *string
		switch key {
		case "cert":
			field = &out.Cert
		case "fingerprint":
			field = &out.Fingerprint
		case "iat-mode":
			field = &out.IATMode
		default:
			continue // not interested
		}
		if len(values) != 1 {
			return nil, fmt.Errorf("obfs4: expected exactly one value for %s", key)
		}
		*field = values[0]
	}
	// Assume that the API knows what it's returning, so don't bother
	// checking whether some fields are missing. If this happens, it
	// will be the obfs4 library task to tell us about that.
	return out, nil
}

// EasyOBFS4ConnectAndHandshake performs a TCP connect to a TCP endpoint
// followed by an OBFS4 handshake. This function is designed to receive
// in input the Tor bridges from the OONI API.
//
// Arguments:
//
// - ctx is the context for deadline/timeout/cancellation;
//
// - timeout is the timeout for the whole operation;
//
// - endpoint is the TCP endpoint to connect to (e.g.,
// 8.8.8.8:443 where the address part of the endpoint MUST
// be an IPv4 or IPv6 address and MUST NOT be a domain);
//
// - dataDir is the data directory to use for obfs4;
//
// - rawParams contains raw obfs4 params from the OONI API.
//
// Returns:
//
// - meas is a JSON serializable OONI measurement (this
// field will never be a nil pointer);
//
// - failure is either nil or a pointer to a OONI failure.
func (mx *Measurer) EasyOBFS4ConnectAndHandshake(ctx context.Context,
	timeout time.Duration, endpoint string, dataDir string,
	rawParams map[string][]string) (meas *ArchivalMeasurement, failure *string) {
	ctx, cancel := context.WithTimeout(ctx, timeout) // honour the timeout
	defer cancel()
	db := &MeasurementDB{}
	params, err := newEasyOBFS4Params(dataDir, rawParams)
	if err != nil {
		failure := err.Error()
		return NewArchivalMeasurement(db.AsMeasurement()), &failure
	}
	conn, err := mx.TCPConnectWithDB(ctx, db, endpoint)
	if err != nil {
		failure := err.Error()
		return NewArchivalMeasurement(db.AsMeasurement()), &failure
	}
	defer conn.Close()
	dialer := netxlite.NewSingleUseDialer(conn)
	obfs4 := ptx.OBFS4Dialer{
		Address:          endpoint,
		Cert:             params.Cert,
		DataDir:          params.DataDir,
		Fingerprint:      params.Fingerprint,
		IATMode:          params.IATMode,
		UnderlyingDialer: dialer,
	}
	o4conn, err := obfs4.DialContext(ctx)
	if err != nil {
		failure := err.Error()
		return NewArchivalMeasurement(db.AsMeasurement()), &failure
	}
	o4conn.Close()
	return NewArchivalMeasurement(db.AsMeasurement()), nil
}
