// Package psiphon implements the psiphon network experiment. This
// implements, in particular, v0.2.0 of the spec.
//
// See https://github.com/ooni/spec/blob/master/nettests/ts-015-psiphon.md
package psiphon

import (
	"context"
	"errors"
	"sync"
	"time"

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

const (
	testName    = "psiphon"
	testVersion = "0.6.0"
)

// Config contains the experiment's configuration.
type Config struct{}

// TestKeys contains the experiment's result.
type TestKeys struct {
	urlgetter.TestKeys
	MaxRuntime float64 `json:"max_runtime"`
}

// Measurer is the psiphon measurer.
type Measurer struct {
	BeforeGetHook func(g urlgetter.Getter)
	Config        Config
}

// ExperimentName returns the experiment name
func (m *Measurer) ExperimentName() string {
	return testName
}

// ExperimentVersion returns the experiment version
func (m *Measurer) ExperimentVersion() string {
	return testVersion
}

func (m *Measurer) printprogress(
	ctx context.Context, wg *sync.WaitGroup,
	maxruntime int, callbacks model.ExperimentCallbacks,
) {
	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()
	step := 1 / float64(maxruntime)
	var progress float64
	defer callbacks.OnProgress(1.0, "psiphon experiment complete")
	defer wg.Done()
	for {
		select {
		case <-ticker.C:
			progress += step
			callbacks.OnProgress(progress, "psiphon experiment running")
		case <-ctx.Done():
			return
		}
	}
}

// Run runs the measurement
func (m *Measurer) Run(
	ctx context.Context, sess model.ExperimentSession,
	measurement *model.Measurement, callbacks model.ExperimentCallbacks,
) error {
	const maxruntime = 300
	ctx, cancel := context.WithTimeout(ctx, maxruntime*time.Second)
	var (
		wg     sync.WaitGroup
		config urlgetter.Config
	)
	wg.Add(1)
	go m.printprogress(ctx, &wg, maxruntime, callbacks)
	config.Tunnel = "psiphon" // force to use psiphon tunnel
	urlgetter.RegisterExtensions(measurement)
	target := "https://www.google.com/humans.txt"
	if measurement.Input != "" {
		target = string(measurement.Input)
	}
	g := urlgetter.Getter{
		Config:  config,
		Session: sess,
		Target:  target,
	}
	if m.BeforeGetHook != nil {
		m.BeforeGetHook(g)
	}
	tk, _ := g.Get(ctx) // ignore error since we have the testkeys and want to submit them
	cancel()
	wg.Wait()
	measurement.TestKeys = &TestKeys{
		TestKeys:   tk,
		MaxRuntime: maxruntime,
	}
	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 {
	BootstrapTime float64 `json:"bootstrap_time"`
	Failure       string  `json:"failure"`
	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")
	}
	if tk.Failure != nil {
		sk.Failure = *tk.Failure
		sk.IsAnomaly = true
	}
	sk.BootstrapTime = tk.BootstrapTime
	return sk, nil
}
