package main

//
// Top-level measurement algorithm
//

import (
	"context"
	"fmt"
	"net"
	"net/url"
	"sync"

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

type (
	// ctrlRequest is the request sent to the test helper
	ctrlRequest = model.THRequest

	// ctrlResponse is the response from the test helper
	ctrlResponse = model.THResponse
)

// measure performs the measurement described by the request and
// returns the corresponding response or an error.
func measure(ctx context.Context, config *handler, creq *ctrlRequest) (*ctrlResponse, error) {
	// create indexed logger
	logger := &indexLogger{
		indexstr: fmt.Sprintf("<#%d> ", config.Indexer.Add(1)),
		logger:   config.BaseLogger,
	}

	// parse input for correctness
	URL, err := url.Parse(creq.HTTPRequest)
	if err != nil {
		logger.Warnf("cannot parse URL: %s", err.Error())
		return nil, err
	}
	wg := &sync.WaitGroup{}

	// dns: start
	dnsch := make(chan ctrlDNSResult, 1)
	if net.ParseIP(URL.Hostname()) == nil {
		wg.Add(1)
		go dnsDo(ctx, &dnsConfig{
			Domain:      URL.Hostname(),
			Logger:      logger,
			NewResolver: config.NewResolver,
			Out:         dnsch,
			Wg:          wg,
		})
	}

	// wait for DNS measurements to complete
	wg.Wait()

	// start assembling the response
	cresp := &ctrlResponse{
		TCPConnect:   map[string]model.THTCPConnectResult{},
		TLSHandshake: map[string]model.THTLSHandshakeResult{},
		HTTPRequest:  model.THHTTPRequestResult{},
		DNS:          model.THDNSResult{},
		IPInfo:       map[string]*model.THIPInfo{},
	}
	select {
	case cresp.DNS = <-dnsch:
	default:
		// we need to emit a non-nil Addrs to match exactly
		// the behavior of the legacy TH
		cresp.DNS = ctrlDNSResult{
			Failure: nil,
			Addrs:   []string{},
			ASNs:    []int64{}, // unused by the TH and not serialized
		}
	}

	// obtain IP info and figure out the endpoints measurement plan
	cresp.IPInfo = newIPInfo(creq, cresp.DNS.Addrs)
	endpoints := ipInfoToEndpoints(URL, cresp.IPInfo)

	// tcpconnect: start over all the endpoints
	tcpconnch := make(chan *tcpResultPair, len(endpoints))
	for _, endpoint := range endpoints {
		wg.Add(1)
		go tcpDo(ctx, &tcpConfig{
			Address:          endpoint.Addr,
			EnableTLS:        endpoint.TLS,
			Endpoint:         endpoint.Epnt,
			Logger:           logger,
			NewDialer:        config.NewDialer,
			NewTSLHandshaker: config.NewTLSHandshaker,
			URLHostname:      URL.Hostname(),
			Out:              tcpconnch,
			Wg:               wg,
		})
	}

	// http: start
	httpch := make(chan ctrlHTTPResponse, 1)
	wg.Add(1)
	go httpDo(ctx, &httpConfig{
		Headers:           creq.HTTPRequestHeaders,
		Logger:            logger,
		MaxAcceptableBody: config.MaxAcceptableBody,
		NewClient:         config.NewClient,
		Out:               httpch,
		URL:               creq.HTTPRequest,
		Wg:                wg,
	})

	// wait for endpoint measurements to complete
	wg.Wait()

	// continue assembling the response
	cresp.HTTPRequest = <-httpch
Loop:
	for {
		select {
		case tcpconn := <-tcpconnch:
			cresp.TCPConnect[tcpconn.Endpoint] = tcpconn.TCP
			if tcpconn.TLS != nil {
				cresp.TLSHandshake[tcpconn.Endpoint] = *tcpconn.TLS
				if info := cresp.IPInfo[tcpconn.Address]; info != nil && tcpconn.TLS.Failure == nil {
					info.Flags |= model.THIPInfoFlagValidForDomain
				}
			}
		default:
			break Loop
		}
	}

	return cresp, nil
}
