// Package fbmessenger contains the Facebook Messenger network experiment.
//
// See https://github.com/ooni/spec/blob/master/nettests/ts-019-facebook-messenger.md
package fbmessenger

import (
	"context"
	"errors"
	"math/rand"
	"time"

	"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
	"github.com/ooni/probe-cli/v3/internal/model"
	"github.com/ooni/probe-cli/v3/internal/netxlite"
)

const (
	// FacebookASN is Facebook's ASN
	FacebookASN = 32934

	// ServiceSTUN is the STUN service
	ServiceSTUN = "dnslookup://stun.fbsbx.com"

	// ServiceBAPI is the b-api service
	ServiceBAPI = "tcpconnect://b-api.facebook.com:443"

	// ServiceBGraph is the b-graph service
	ServiceBGraph = "tcpconnect://b-graph.facebook.com:443"

	// ServiceEdge is the edge service
	ServiceEdge = "tcpconnect://edge-mqtt.facebook.com:443"

	// ServiceExternalCDN is the external CDN service
	ServiceExternalCDN = "tcpconnect://external.xx.fbcdn.net:443"

	// ServiceScontentCDN is the scontent CDN service
	ServiceScontentCDN = "tcpconnect://scontent.xx.fbcdn.net:443"

	// ServiceStar is the star service
	ServiceStar = "tcpconnect://star.c10r.facebook.com:443"

	testName    = "facebook_messenger"
	testVersion = "0.2.0"
)

// Config contains the experiment config.
type Config struct{}

// TestKeys contains the experiment results
type TestKeys struct {
	urlgetter.TestKeys
	FacebookBAPIDNSConsistent        *bool `json:"facebook_b_api_dns_consistent"`
	FacebookBAPIReachable            *bool `json:"facebook_b_api_reachable"`
	FacebookBGraphDNSConsistent      *bool `json:"facebook_b_graph_dns_consistent"`
	FacebookBGraphReachable          *bool `json:"facebook_b_graph_reachable"`
	FacebookEdgeDNSConsistent        *bool `json:"facebook_edge_dns_consistent"`
	FacebookEdgeReachable            *bool `json:"facebook_edge_reachable"`
	FacebookExternalCDNDNSConsistent *bool `json:"facebook_external_cdn_dns_consistent"`
	FacebookExternalCDNReachable     *bool `json:"facebook_external_cdn_reachable"`
	FacebookScontentCDNDNSConsistent *bool `json:"facebook_scontent_cdn_dns_consistent"`
	FacebookScontentCDNReachable     *bool `json:"facebook_scontent_cdn_reachable"`
	FacebookStarDNSConsistent        *bool `json:"facebook_star_dns_consistent"`
	FacebookStarReachable            *bool `json:"facebook_star_reachable"`
	FacebookSTUNDNSConsistent        *bool `json:"facebook_stun_dns_consistent"`
	FacebookSTUNReachable            *bool `json:"facebook_stun_reachable"`
	FacebookDNSBlocking              *bool `json:"facebook_dns_blocking"`
	FacebookTCPBlocking              *bool `json:"facebook_tcp_blocking"`
}

// Update updates the TestKeys using the given MultiOutput result.
func (tk *TestKeys) Update(v urlgetter.MultiOutput) {
	// Update the easy to update entries first
	tk.NetworkEvents = append(tk.NetworkEvents, v.TestKeys.NetworkEvents...)
	tk.Queries = append(tk.Queries, v.TestKeys.Queries...)
	tk.Requests = append(tk.Requests, v.TestKeys.Requests...)
	tk.TCPConnect = append(tk.TCPConnect, v.TestKeys.TCPConnect...)
	tk.TLSHandshakes = append(tk.TLSHandshakes, v.TestKeys.TLSHandshakes...)
	// Set the status of endpoints
	switch v.Input.Target {
	case ServiceSTUN:
		var ignored *bool
		tk.ComputeEndpointStatus(v, &tk.FacebookSTUNDNSConsistent, &ignored)
	case ServiceBAPI:
		tk.ComputeEndpointStatus(
			v, &tk.FacebookBAPIDNSConsistent, &tk.FacebookBAPIReachable)
	case ServiceBGraph:
		tk.ComputeEndpointStatus(
			v, &tk.FacebookBGraphDNSConsistent, &tk.FacebookBGraphReachable)
	case ServiceEdge:
		tk.ComputeEndpointStatus(
			v, &tk.FacebookEdgeDNSConsistent, &tk.FacebookEdgeReachable)
	case ServiceExternalCDN:
		tk.ComputeEndpointStatus(
			v, &tk.FacebookExternalCDNDNSConsistent, &tk.FacebookExternalCDNReachable)
	case ServiceScontentCDN:
		tk.ComputeEndpointStatus(
			v, &tk.FacebookScontentCDNDNSConsistent, &tk.FacebookScontentCDNReachable)
	case ServiceStar:
		tk.ComputeEndpointStatus(
			v, &tk.FacebookStarDNSConsistent, &tk.FacebookStarReachable)
	}
}

var (
	trueValue  = true
	falseValue = false
)

// ComputeEndpointStatus computes the DNS and TCP status of a specific endpoint.
func (tk *TestKeys) ComputeEndpointStatus(v urlgetter.MultiOutput, dns, tcp **bool) {
	// start where all is unknown
	*dns, *tcp = nil, nil
	// process DNS first
	if v.TestKeys.FailedOperation != nil && *v.TestKeys.FailedOperation == netxlite.ResolveOperation {
		tk.FacebookDNSBlocking = &trueValue
		*dns = &falseValue
		return // we know that the DNS has failed
	}
	for _, query := range v.TestKeys.Queries {
		for _, ans := range query.Answers {
			if ans.ASN != FacebookASN {
				tk.FacebookDNSBlocking = &trueValue
				*dns = &falseValue
				return // because DNS is lying
			}
		}
	}
	*dns = &trueValue
	// now process connect
	if v.TestKeys.FailedOperation != nil && *v.TestKeys.FailedOperation == netxlite.ConnectOperation {
		tk.FacebookTCPBlocking = &trueValue
		*tcp = &falseValue
		return // because connect failed
	}
	// all good
	*tcp = &trueValue
}

// Measurer performs the measurement
type Measurer struct {
	// Config contains the experiment settings. If empty we
	// will be using default settings.
	Config Config

	// Getter is an optional getter to be used for testing.
	Getter urlgetter.MultiGetter
}

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

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

// Run implements ExperimentMeasurer.Run
func (m Measurer) Run(
	ctx context.Context, sess model.ExperimentSession,
	measurement *model.Measurement, callbacks model.ExperimentCallbacks,
) error {
	ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
	defer cancel()
	urlgetter.RegisterExtensions(measurement)
	// generate targets
	services := []string{
		ServiceSTUN, ServiceBAPI, ServiceBGraph, ServiceEdge, ServiceExternalCDN,
		ServiceScontentCDN, ServiceStar,
	}
	var inputs []urlgetter.MultiInput
	for _, service := range services {
		inputs = append(inputs, urlgetter.MultiInput{Target: service})
	}
	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
	rnd.Shuffle(len(inputs), func(i, j int) {
		inputs[i], inputs[j] = inputs[j], inputs[i]
	})
	// measure in parallel
	multi := urlgetter.Multi{Begin: time.Now(), Getter: m.Getter, Session: sess}
	testkeys := new(TestKeys)
	testkeys.Agent = "redirect"
	measurement.TestKeys = testkeys
	for entry := range multi.Collect(ctx, inputs, "facebook_messenger", callbacks) {
		testkeys.Update(entry)
	}
	// if we haven't yet determined the status of DNS blocking and TCP blocking
	// then no blocking has been detected and we can set them
	if testkeys.FacebookDNSBlocking == nil {
		testkeys.FacebookDNSBlocking = &falseValue
	}
	if testkeys.FacebookTCPBlocking == nil {
		testkeys.FacebookTCPBlocking = &falseValue
	}
	return nil
}

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

// SummaryKeys contains summary keys for this experiment.
//
// Note that this structure is part of the ABI contract with ooniprobe
// therefore we should be careful when changing it.
type SummaryKeys struct {
	DNSBlocking bool `json:"facebook_dns_blocking"`
	TCPBlocking bool `json:"facebook_tcp_blocking"`
	IsAnomaly   bool `json:"-"`
}

// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
func (m Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
	sk := SummaryKeys{IsAnomaly: false}
	tk, ok := measurement.TestKeys.(*TestKeys)
	if !ok {
		return sk, errors.New("invalid test keys type")
	}
	dnsBlocking := tk.FacebookDNSBlocking != nil && *tk.FacebookDNSBlocking
	tcpBlocking := tk.FacebookTCPBlocking != nil && *tk.FacebookTCPBlocking
	sk.DNSBlocking = dnsBlocking
	sk.TCPBlocking = tcpBlocking
	sk.IsAnomaly = dnsBlocking || tcpBlocking
	return sk, nil
}
