// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package manager

import (
	"context"
	"crypto/tls"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"time"

	"github.com/gitpod-io/gitpod/common-go/log"
	"github.com/gitpod-io/gitpod/common-go/tracing"
	"golang.org/x/xerrors"
	"k8s.io/apimachinery/pkg/util/json"
)

// WorkspaceProbeResult marks the result of a workspace probe
type WorkspaceProbeResult string

const (
	// WorkspaceProbeReady means that the probe found the workspace ready in time
	WorkspaceProbeReady WorkspaceProbeResult = "ready"

	// WorkspaceProbeStopped means that the probe (and probably the) workspace itself was stopped
	WorkspaceProbeStopped WorkspaceProbeResult = "stopped"
)

// WorkspaceReadyProbe repeatedly checks if a workspace reports as ready
type WorkspaceReadyProbe struct {
	Timeout    time.Duration
	RetryDelay time.Duration

	bypass      func() WorkspaceProbeResult
	readyURL    string
	workspaceID string
}

// NewWorkspaceReadyProbe creates a new workspace probe
func NewWorkspaceReadyProbe(workspaceID string, workspaceURL url.URL) WorkspaceReadyProbe {
	workspaceURL.Path += "/_supervisor/v1/status/ide/wait/true"
	readyURL := workspaceURL.String()

	return WorkspaceReadyProbe{
		Timeout:     5 * time.Second,
		RetryDelay:  500 * time.Millisecond,
		readyURL:    readyURL,
		workspaceID: workspaceID,
	}
}

// Run starts the probe. This function returns when the probe finishes, times out or is stopped.
func (p *WorkspaceReadyProbe) Run(ctx context.Context) WorkspaceProbeResult {
	if p.bypass != nil {
		// Note: the probe bypass is used during testing only.
		//       If you ever see this log line in production we have a configuration issue!
		log.WithFields(log.OWI("", "", p.workspaceID)).Warn("not actually probing workspace but using bypass")
		return p.bypass()
	}

	owi := log.OWI("", "", p.workspaceID)
	log := log.WithFields(owi)
	span, ctx := tracing.FromContext(ctx, "WorkspaceReadyProbe.Run")
	span.SetTag("timeout", p.Timeout)
	tracing.ApplyOWI(span, owi)
	defer tracing.FinishSpan(span, nil)

	client := &http.Client{
		Timeout: p.Timeout,
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
			DialContext: (&net.Dialer{
				Timeout:   1 * time.Second,
				KeepAlive: -1 * time.Second,
				DualStack: false,
			}).DialContext,
			MaxIdleConns:          0,
			MaxIdleConnsPerHost:   32,
			IdleConnTimeout:       10 * time.Second,
			TLSHandshakeTimeout:   10 * time.Second,
			ExpectContinueTimeout: 5 * time.Second,
			DisableKeepAlives:     true,
		},
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	for {
		if ctx.Err() != nil {
			return WorkspaceProbeStopped
		}

		result, err := callWorkspaceProbe(p.readyURL, client)
		if err != nil {
			urlerr, ok := err.(*url.Error)
			if !ok || !urlerr.Timeout() {
				log.WithError(err).Debug("got a non-timeout error during workspace probe")
				time.Sleep(p.RetryDelay)
				continue
			}

			// we've timed out - do not log this as it would spam the logs for no good reason
			continue
		}

		if !result.Ok {
			log.WithField("url", p.readyURL).Debug("workspace did not respond to ready probe with OK status")
			time.Sleep(p.RetryDelay)
			continue
		}

		break
	}

	// workspace is actually ready
	return WorkspaceProbeReady
}

type probeResult struct {
	Ok bool `json:"ok"`
}

func callWorkspaceProbe(url string, client *http.Client) (*probeResult, error) {
	resp, err := client.Get(url)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, xerrors.Errorf("workspace probe request returned %v as status code", resp.StatusCode)
	}

	rawBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var result probeResult
	err = json.Unmarshal(rawBody, &result)
	if err != nil {
		return nil, err
	}

	return &result, nil
}
