package model

//
// Definition of experiment and types used by the
// implementation of all experiments.
//

import (
	"context"
)

// ExperimentSession is the experiment's view of a session.
type ExperimentSession interface {
	// GetTestHelpersByName returns a list of test helpers with the given name.
	GetTestHelpersByName(name string) ([]OOAPIService, bool)

	// DefaultHTTPClient returns the default HTTPClient used by the session.
	DefaultHTTPClient() HTTPClient

	// FetchPsiphonConfig returns psiphon's config as a serialized JSON or an error.
	FetchPsiphonConfig(ctx context.Context) ([]byte, error)

	// FetchTorTargets returns the targets for the Tor experiment or an error.
	FetchTorTargets(ctx context.Context, cc string) (map[string]OOAPITorTarget, error)

	// Logger returns the logger used by the session.
	Logger() Logger

	// ProbeCC returns the country code.
	ProbeCC() string

	// ResolverIP returns the resolver's IP.
	ResolverIP() string

	// TempDir returns the session's temporary directory.
	TempDir() string

	// TorArgs returns the arguments we should pass to tor when executing it.
	TorArgs() []string

	// TorBinary returns the path of the tor binary.
	TorBinary() string

	// TunnelDir is the directory where to store tunnel information.
	TunnelDir() string

	// UserAgent returns the user agent we should be using when we're fine
	// with identifying ourselves as ooniprobe.
	UserAgent() string
}

// ExperimentAsyncTestKeys is the type of test keys returned by an experiment
// when running in async fashion rather than in sync fashion.
type ExperimentAsyncTestKeys struct {
	// Extensions contains the extensions used by this experiment.
	Extensions map[string]int64

	// Input is the input this measurement refers to.
	Input MeasurementTarget

	// MeasurementRuntime is the total measurement runtime.
	MeasurementRuntime float64

	// TestHelpers contains the test helpers used in the experiment
	TestHelpers map[string]interface{}

	// TestKeys contains the actual test keys.
	TestKeys interface{}
}

// ExperimentMeasurerAsync is a measurer that can run in async fashion.
//
// Currently this functionality is optional, but we will likely
// migrate all experiments to use this functionality in 2022.
type ExperimentMeasurerAsync interface {
	// RunAsync runs the experiment in async fashion.
	//
	// Arguments:
	//
	// - ctx is the context for deadline/timeout/cancellation
	//
	// - sess is the measurement session
	//
	// - input is the input URL to measure
	//
	// - callbacks contains the experiment callbacks
	//
	// Returns either a channel where TestKeys are posted or an error.
	//
	// An error indicates that specific preconditions for running the experiment
	// are not met (e.g., the input URL is invalid).
	//
	// On success, the experiment will post on the channel each new
	// measurement until it is done and closes the channel.
	RunAsync(ctx context.Context, sess ExperimentSession, input string,
		callbacks ExperimentCallbacks) (<-chan *ExperimentAsyncTestKeys, error)
}

// ExperimentCallbacks contains experiment event-handling callbacks
type ExperimentCallbacks interface {
	// OnProgress provides information about an experiment progress.
	OnProgress(percentage float64, message string)
}

// PrinterCallbacks is the default event handler
type PrinterCallbacks struct {
	Logger
}

// NewPrinterCallbacks returns a new default callback handler
func NewPrinterCallbacks(logger Logger) PrinterCallbacks {
	return PrinterCallbacks{Logger: logger}
}

// OnProgress provides information about an experiment progress.
func (d PrinterCallbacks) OnProgress(percentage float64, message string) {
	d.Logger.Infof("[%5.1f%%] %s", percentage*100, message)
}

// ExperimentMeasurer is the interface that allows to run a
// measurement for a specific experiment.
type ExperimentMeasurer interface {
	// ExperimentName returns the experiment name.
	ExperimentName() string

	// ExperimentVersion returns the experiment version.
	ExperimentVersion() string

	// Run runs the experiment with the specified context, session,
	// measurement, and experiment calbacks. This method should only
	// return an error in case the experiment could not run (e.g.,
	// a required input is missing). Otherwise, the code should just
	// set the relevant OONI error inside of the measurement and
	// return nil. This is important because the caller WILL NOT submit
	// the measurement if this method returns an error.
	Run(
		ctx context.Context, sess ExperimentSession,
		measurement *Measurement, callbacks ExperimentCallbacks,
	) error

	// GetSummaryKeys returns summary keys expected by ooni/probe-cli.
	GetSummaryKeys(*Measurement) (interface{}, error)
}

// Experiment is an experiment instance.
type Experiment interface {
	// KibiBytesReceived accounts for the KibiBytes received by the experiment.
	KibiBytesReceived() float64

	// KibiBytesSent is like KibiBytesReceived but for the bytes sent.
	KibiBytesSent() float64

	// Name returns the experiment name.
	Name() string

	// GetSummaryKeys returns a data structure containing a
	// summary of the test keys for ooniprobe.
	GetSummaryKeys(m *Measurement) (any, error)

	// ReportID returns the open report's ID, if we have opened a report
	// successfully before, or an empty string, otherwise.
	//
	// Deprecated: new code should use a Submitter.
	ReportID() string

	// MeasureAsync runs an async measurement. This operation could post
	// one or more measurements onto the returned channel. We'll close the
	// channel when we've emitted all the measurements.
	//
	// Arguments:
	//
	// - ctx is the context for deadline/cancellation/timeout;
	//
	// - input is the input (typically a URL but it could also be
	// just an endpoint or an empty string for input-less experiments
	// such as, e.g., ndt7 and dash).
	//
	// Return value:
	//
	// - on success, channel where to post measurements (the channel
	// will be closed when done) and nil error;
	//
	// - on failure, nil channel and non-nil error.
	MeasureAsync(ctx context.Context, input string) (<-chan *Measurement, error)

	// MeasureWithContext performs a synchronous measurement.
	//
	// Return value: strictly either a non-nil measurement and
	// a nil error or a nil measurement and a non-nil error.
	//
	// CAVEAT: while this API is perfectly fine for experiments that
	// return a single measurement, it will only return the first measurement
	// when used with an asynchronous experiment.
	MeasureWithContext(ctx context.Context, input string) (measurement *Measurement, err error)

	// SaveMeasurement saves a measurement on the specified file path.
	//
	// Deprecated: new code should use a Saver.
	SaveMeasurement(measurement *Measurement, filePath string) error

	// SubmitAndUpdateMeasurementContext submits a measurement and updates the
	// fields whose value has changed as part of the submission.
	//
	// Deprecated: new code should use a Submitter.
	SubmitAndUpdateMeasurementContext(
		ctx context.Context, measurement *Measurement) error

	// OpenReportContext will open a report using the given context
	// to possibly limit the lifetime of this operation.
	//
	// Deprecated: new code should use a Submitter.
	OpenReportContext(ctx context.Context) error
}

// InputPolicy describes the experiment policy with respect to input. That is
// whether it requires input, optionally accepts input, does not want input.
type InputPolicy string

const (
	// InputOrQueryBackend indicates that the experiment requires
	// external input to run and that this kind of input is URLs
	// from the citizenlab/test-lists repository. If this input
	// not provided to the experiment, then the code that runs the
	// experiment is supposed to fetch from URLs from OONI's backends.
	InputOrQueryBackend = InputPolicy("or_query_backend")

	// InputStrictlyRequired indicates that the experiment
	// requires input and we currently don't have an API for
	// fetching such input. Therefore, either the user specifies
	// input or the experiment will fail for the lack of input.
	InputStrictlyRequired = InputPolicy("strictly_required")

	// InputOptional indicates that the experiment handles input,
	// if any; otherwise it fetchs input/uses a default.
	InputOptional = InputPolicy("optional")

	// InputNone indicates that the experiment does not want any
	// input and ignores the input if provided with it.
	InputNone = InputPolicy("none")

	// We gather input from StaticInput and SourceFiles. If there is
	// input, we return it. Otherwise, we return an internal static
	// list of inputs to be used with this experiment.
	InputOrStaticDefault = InputPolicy("or_static_default")
)

// ExperimentBuilder builds an experiment.
type ExperimentBuilder interface {
	// Interruptible tells you whether this is an interruptible experiment. This kind
	// of experiments (e.g. ndt7) may be interrupted mid way.
	Interruptible() bool

	// InputPolicy returns the experiment input policy.
	InputPolicy() InputPolicy

	// Options returns information about the experiment's options.
	Options() (map[string]ExperimentOptionInfo, error)

	// SetOptionAny sets an option whose value is an any value. We will use reasonable
	// heuristics to convert the any value to the proper type of the field whose name is
	// contained by the key variable. If we cannot convert the provided any value to
	// the proper type, then this function returns an error.
	SetOptionAny(key string, value any) error

	// SetOptionsAny sets options from a map[string]any. See the documentation of
	// the SetOptionAny method for more information.
	SetOptionsAny(options map[string]any) error

	// SetCallbacks sets the experiment's interactive callbacks.
	SetCallbacks(callbacks ExperimentCallbacks)

	// NewExperiment creates the experiment instance.
	NewExperiment() Experiment
}

// ExperimentOptionInfo contains info about an experiment option.
type ExperimentOptionInfo struct {
	// Doc contains the documentation.
	Doc string

	// Type contains the type.
	Type string
}

// ExperimentInputLoader loads inputs from local or remote sources.
type ExperimentInputLoader interface {
	Load(ctx context.Context) ([]OOAPIURLInfo, error)
}

// Submitter submits a measurement to the OONI collector.
type Submitter interface {
	// Submit submits the measurement and updates its
	// report ID field in case of success.
	Submit(ctx context.Context, m *Measurement) error
}

// Saver saves a measurement on some persistent storage.
type Saver interface {
	SaveMeasurement(m *Measurement) error
}

// ExperimentInputProcessor processes inputs for an experiment.
type ExperimentInputProcessor interface {
	Run(ctx context.Context) error
}
