package internal

import (
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"strconv"
	"time"

	"github.com/onsi/gomega/gbytes"
	"github.com/onsi/gomega/gexec"
	"github.com/pkg/errors"

	"openebs.io/metac/test/integration/framework/addr"
)

// ProcessState define the state of the process.
type ProcessState struct {
	DefaultedProcessInput

	Session *gexec.Session

	// Healthcheck Endpoint. If we get http.StatusOK from this endpoint,
	// we assume the process is ready to operate. E.g. "/healthz". If
	// this is set,we ignore StartMessage.
	HealthCheckEndpoint string

	// HealthCheckPollInterval is the interval which will be used for
	// polling the HealthCheckEndpoint.
	//
	// If left empty it will default to 100 Milliseconds.
	HealthCheckPollInterval time.Duration

	// StartMessage is the message to wait for on stderr. If we
	// receive this message, we assume the process is ready to
	// operate. Ignored if HealthCheckEndpoint is specified.
	//
	// The usage of StartMessage is discouraged, favour
	// HealthCheckEndpoint instead!
	//
	// Deprecated: Use HealthCheckEndpoint in favour of StartMessage
	StartMessage string

	Args []string

	// ready holds whether the process is currently in ready state
	// (hit the ready condition) or not.
	// It will be set to true on a successful `Start()` and set to
	// false on a successful `Stop()`
	ready bool
}

// DefaultedProcessInput defines the default process input required to perform the test.
type DefaultedProcessInput struct {
	URL              url.URL
	Dir              string
	DirNeedsCleaning bool
	Path             string
	StopTimeout      time.Duration
	StartTimeout     time.Duration
}

// DoDefaulting sets the default configuration according to the
// data informed and return an DefaultedProcessInput
// and an error if some requirement was not informed.
func DoDefaulting(
	name string,
	listenURL *url.URL,
	dir string,
	path string,
	startTimeout time.Duration,
	stopTimeout time.Duration,
) (DefaultedProcessInput, error) {
	defaults := DefaultedProcessInput{
		Dir:          dir,
		Path:         path,
		StartTimeout: startTimeout,
		StopTimeout:  stopTimeout,
	}

	if listenURL == nil {
		port, host, err := addr.Suggest()
		if err != nil {
			return DefaultedProcessInput{}, err
		}
		defaults.URL = url.URL{
			Scheme: "http",
			Host: net.JoinHostPort(
				host,
				strconv.Itoa(port),
			),
		}
	} else {
		defaults.URL = *listenURL
	}

	if dir == "" {
		newDir, err := ioutil.TempDir("", "k8s_test_framework_")
		if err != nil {
			return DefaultedProcessInput{}, err
		}
		defaults.Dir = newDir
		defaults.DirNeedsCleaning = true
	}

	if path == "" {
		if name == "" {
			return DefaultedProcessInput{},
				fmt.Errorf("Must have at least name or path")
		}
		defaults.Path = BinPathFinder(name)
	}

	if startTimeout == 0 {
		defaults.StartTimeout = 20 * time.Second
	}

	if stopTimeout == 0 {
		defaults.StopTimeout = 20 * time.Second
	}

	return defaults, nil
}

type stopChannel chan struct{}

// Start starts the process, waits for it to come up,
// and returns an error if any
func (ps *ProcessState) Start(stdout, stderr io.Writer) (err error) {
	if ps.ready {
		return nil
	}

	command := exec.Command(ps.Path, ps.Args...)

	ready := make(chan bool)
	timedOut := time.After(ps.StartTimeout)
	var pollerStopCh stopChannel

	if ps.HealthCheckEndpoint != "" {
		healthCheckURL := ps.URL
		healthCheckURL.Path = ps.HealthCheckEndpoint
		pollerStopCh = make(stopChannel)
		go pollURLUntilOK(
			healthCheckURL,
			ps.HealthCheckPollInterval,
			ready,
			pollerStopCh,
		)
	} else {
		startDetectStream := gbytes.NewBuffer()
		ready = startDetectStream.Detect(ps.StartMessage)
		stderr = safeMultiWriter(stderr, startDetectStream)
	}

	ps.Session, err = gexec.Start(command, stdout, stderr)
	if err != nil {
		return err
	}

	select {
	case <-ready:
		ps.ready = true
		return nil
	case <-timedOut:
		if pollerStopCh != nil {
			close(pollerStopCh)
		}
		if ps.Session != nil {
			ps.Session.Terminate()
		}
		return errors.Errorf(
			"Failed to start %s: Timedout %s",
			ps.Path,
			ps.StartTimeout,
		)
	}
}

func safeMultiWriter(writers ...io.Writer) io.Writer {
	safeWriters := []io.Writer{}
	for _, w := range writers {
		if w != nil {
			safeWriters = append(safeWriters, w)
		}
	}
	return io.MultiWriter(safeWriters...)
}

func pollURLUntilOK(
	url url.URL,
	interval time.Duration,
	ready chan bool,
	stopCh stopChannel,
) {
	if interval <= 0 {
		interval = 100 * time.Millisecond
	}
	for {
		res, err := http.Get(url.String())
		if err == nil {
			res.Body.Close()
			if res.StatusCode == http.StatusOK {
				ready <- true
				return
			}
		}

		select {
		case <-stopCh:
			return
		default:
			time.Sleep(interval)
		}
	}
}

// Stop stops this process gracefully, waits for its termination,
// and cleans up the CertDir if necessary.
func (ps *ProcessState) Stop() error {
	if ps.Session == nil {
		return nil
	}

	// gexec's Session methods (Signal, Kill, ...) do not check
	// if the Process is nil, so we are doing this here for now.
	// This should probably be fixed in gexec.
	if ps.Session.Command.Process == nil {
		return nil
	}

	detectedStop := ps.Session.Terminate().Exited
	timedOut := time.After(ps.StopTimeout)

	select {
	case <-detectedStop:
		break
	case <-timedOut:
		return errors.Errorf(
			"Failed to stop %s: Timedout %s",
			ps.Path,
			ps.StopTimeout,
		)
	}
	ps.ready = false
	if ps.DirNeedsCleaning {
		return os.RemoveAll(ps.Dir)
	}

	return nil
}
