package tlsmiddlebox

//
// Measurer
//

import (
	"context"
	"errors"
	"net/url"
	"sync"
	"time"

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

const (
	testName    = "tlsmiddlebox"
	testVersion = "0.1.0"
)

// Measurer performs the measurement.
type Measurer struct {
	config Config
}

// ExperimentName implements ExperimentMeasurer.ExperimentName.
func (m *Measurer) ExperimentName() string {
	return testName
}

// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion.
func (m *Measurer) ExperimentVersion() string {
	return testVersion
}

var (
	// errNoInputProvided indicates you didn't provide any input
	errNoInputProvided = errors.New("no input provided")

	// errInputIsNotAnURL indicates that input is not an URL
	errInputIsNotAnURL = errors.New("input is not an URL")

	// errInvalidInputScheme indicates that the input scheme is invalid
	errInvalidInputScheme = errors.New("input scheme must be tlstrace")

	// errInvalidTestHelper indicates that the testhelper is invalid
	errInvalidTestHelper = errors.New("invalid testhelper")

	// errInvalidTHScheme indicates that the TH scheme is invalid
	errInvalidTHScheme = errors.New("th scheme must be tlshandshake")
)

// // Run implements ExperimentMeasurer.Run.
func (m *Measurer) Run(
	ctx context.Context,
	sess model.ExperimentSession,
	measurement *model.Measurement,
	callbacks model.ExperimentCallbacks,
) error {
	if measurement.Input == "" {
		return errNoInputProvided
	}
	parsed, err := url.Parse(string(measurement.Input))
	if err != nil {
		return errInputIsNotAnURL
	}
	if parsed.Scheme != "tlstrace" {
		return errInvalidInputScheme
	}
	th, err := m.config.testhelper(parsed.Host)
	if err != nil {
		return errInvalidTestHelper
	}
	if th.Scheme != "tlshandshake" {
		return errInvalidTHScheme
	}
	tk := NewTestKeys()
	measurement.TestKeys = tk
	wg := new(sync.WaitGroup)
	// 1. perform a DNSLookup
	addrs, err := m.DNSLookup(ctx, 0, measurement.MeasurementStartTimeSaved, sess.Logger(), th.Hostname(), tk)
	if err != nil {
		return err
	}
	// 2. measure addresses
	addrs = prepareAddrs(addrs, th.Port())
	for i, addr := range addrs {
		wg.Add(1)
		go m.TraceAddress(ctx, int64(i), measurement.MeasurementStartTimeSaved, sess.Logger(), addr, parsed.Hostname(), tk, wg)
	}
	wg.Wait()
	return nil
}

// TraceAddress measures a single address after the DNSLookup
func (m *Measurer) TraceAddress(ctx context.Context, index int64, zeroTime time.Time, logger model.Logger,
	address string, sni string, tk *TestKeys, wg *sync.WaitGroup) error {
	defer wg.Done()
	trace := &CompleteTrace{
		Address: address,
	}
	tk.addTrace(trace)
	err := m.TCPConnect(ctx, index, zeroTime, logger, address, tk)
	if err != nil {
		return err // skip tracing if we cannot connect with default TTL
	}
	m.TLSTrace(ctx, index, zeroTime, logger, address, sni, trace)
	return nil
}

// NewExperimentMeasurer creates a new ExperimentMeasurer.
func NewExperimentMeasurer(config Config) *Measurer {
	return &Measurer{config: config}
}
