// 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 dart

import (
	"bytes"
	"context"
	"fmt"
	"math/rand"
	"net/http"
	"net/url"
	"strings"
	"sync"

	toxiproxy "github.com/Shopify/toxiproxy/client"
	log "github.com/sirupsen/logrus"
	"golang.org/x/xerrors"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/portforward"
	"k8s.io/client-go/transport/spdy"
)

const (
	// port to toxiproxy's control plane API
	toxiproxyPort = 8474
)

// ProxiedToxiproxy provides a connection to a toxiproxy pod
type ProxiedToxiproxy struct {
	*toxiproxy.Client
	closer func()
}

// NewProxiedToxiproxy connects to a Toxiproxy pod
func NewProxiedToxiproxy(cfg *rest.Config, namespace, pod string) (*ProxiedToxiproxy, error) {
	localPort := rand.Intn(2000) + 31000

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	readychan, errchan := forwardPort(ctx, cfg, namespace, pod, fmt.Sprintf("%d:%d", localPort, toxiproxyPort))
	select {
	case <-readychan:
	case err := <-errchan:
		return nil, err
	}
	log.WithField("port", localPort).Info("forwarding control API of toxiproxy")

	tpc := toxiproxy.NewClient(fmt.Sprintf("localhost:%d", localPort))
	return &ProxiedToxiproxy{
		Client: tpc,
		closer: cancel,
	}, nil
}

// Close shuts down the connection to the toxiproxy pod
func (p *ProxiedToxiproxy) Close() error {
	p.closer()
	return nil
}

// ForwardPort establishes a TCP port forwarding to a Kubernetes pod
func forwardPort(ctx context.Context, config *rest.Config, namespace, pod, port string) (readychan chan struct{}, errchan chan error) {
	errchan = make(chan error, 1)
	readychan = make(chan struct{}, 1)

	roundTripper, upgrader, err := spdy.RoundTripperFor(config)
	if err != nil {
		errchan <- err
		return
	}

	path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, pod)
	hostIP := strings.TrimPrefix(config.Host, "https://")
	serverURL := url.URL{Scheme: "https", Path: path, Host: hostIP}
	dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, &serverURL)

	stopChan := make(chan struct{}, 1)
	fwdReadyChan := make(chan struct{}, 1)
	out, errOut := new(bytes.Buffer), new(bytes.Buffer)
	forwarder, err := portforward.New(dialer, []string{port}, stopChan, fwdReadyChan, out, errOut)
	if err != nil {
		panic(err)
	}

	var once sync.Once
	go func() {
		err := forwarder.ForwardPorts()
		if err != nil {
			errchan <- err
		}
		once.Do(func() { close(readychan) })
	}()

	go func() {
		select {
		case <-readychan:
			// we're out of here
		case <-ctx.Done():
			close(stopChan)
		}
	}()

	go func() {
		for range fwdReadyChan {
		}

		if errOut.Len() != 0 {
			errchan <- xerrors.Errorf(errOut.String())
			return
		}

		once.Do(func() { close(readychan) })
	}()

	return
}
