package testbed

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	errorspkg "github.com/pkg/errors"

	"github.com/cheynewallace/tabby"
	pkgErrors "github.com/pkg/errors"
	"k8s.io/apimachinery/pkg/util/wait"

	"github.com/pingcap/test-infra/common/errors"
	"github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/common/model/rms/request"
	"github.com/pingcap/test-infra/common/model/rms/response"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/config"
	imagepkg "github.com/pingcap/test-infra/tools/tcctl/pkg/image"
	transpkg "github.com/pingcap/test-infra/tools/tcctl/pkg/transform"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/util"
)

type Testbed struct {
	ServerConfig config.ApiServerConfig
	ECRServer    string

	ecrImageSyncer imagepkg.ECRImageSyncer
	imageScanner   transpkg.ImageScanner
}

func (t *Testbed) Init() *Testbed {
	t.ecrImageSyncer = imagepkg.ECRImageSyncer{ServerConfig: t.ServerConfig, EcrServer: t.ECRServer, SyncToECRImages: make(map[string]bool)}
	t.imageScanner = transpkg.ImageScanner{ServerConfig: t.ServerConfig}
	return t
}

// TODO: (mahjonp) move these into service/rms/client
func (t *Testbed) Create(testbedRequest *request.CreateTestbedRequest) (*response.CreateTestbedResponse, error) {
	err := t.collectAndUpdateImagesToECR(testbedRequest)
	if err != nil {
		return nil, errorspkg.Wrap(err, "collect images error")
	}
	err = t.syncECRImages()
	if err != nil {
		return nil, errorspkg.Wrap(err, "sync images to ECR error")
	}
	r, err := util.Req().
		Host(t.ServerConfig.RmsHost).
		Path("api/v1/testbeds").
		Json(testbedRequest).
		Bearer(t.ServerConfig.Token).
		Client(&http.Client{Timeout: 30 * time.Second}).
		Post()
	if err != nil {
		return nil, err
	}
	b, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return nil, err
	}
	switch r.StatusCode {
	case http.StatusOK:
		res := &response.CreateTestbedResponse{}
		if err := json.Unmarshal(b, res); err != nil {
			return nil, err
		}
		return res, nil
	}
	statusErr := new(errors.StatusError)
	if err := json.Unmarshal(b, statusErr); err != nil {
		return nil, err
	}
	return nil, statusErr
}

func (t *Testbed) collectAndUpdateImagesToECR(testbed *request.CreateTestbedRequest) error {
	if !t.shouldSyncImagesToECR(testbed) {
		return nil
	}
	images, err := t.imageScanner.ScanImagesOnTestbed(&testbed.TestBed, t.ecrImageSyncer.RewriteToEcrImage)
	if err != nil {
		return err
	}
	for image := range images {
		t.addImageToECR(image)
	}
	return nil
}

func (t *Testbed) shouldSyncImagesToECR(testbed *request.CreateTestbedRequest) bool {
	return strings.HasPrefix(strings.ToLower(testbed.ResourcePool), "aws-") || strings.HasPrefix(strings.ToLower(testbed.ResourcePool), "eks-")
}

func (t *Testbed) addImageToECR(image string) {
	t.ecrImageSyncer.SyncToECRImages[image] = true
}

func (t *Testbed) syncECRImages() error {
	return t.ecrImageSyncer.Run()
}

func (t *Testbed) Get(testbed string) (*response.GetTestbedResponse, error) {
	client := http.Client{Timeout: time.Second * 30}
	res, err := util.Req().
		Host(t.ServerConfig.RmsHost).
		Path("api/v1/testbeds/" + testbed).
		Client(&client).
		Bearer(t.ServerConfig.Token).
		Get()

	if err != nil {
		return nil, err
	}
	b, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	switch res.StatusCode {
	case http.StatusOK:
		testbedResponse := &response.GetTestbedResponse{}
		if err := json.Unmarshal(b, testbedResponse); err != nil {
			return nil, err
		}
		return testbedResponse, nil
	}
	statusErr := new(errors.StatusError)
	if err := json.Unmarshal(b, statusErr); err != nil {
		return nil, err
	}
	return nil, statusErr
}

func (t *Testbed) testbedFailed(testbed string) (message string, failed bool) {
	resp, err := t.Check(testbed)
	if err != nil {
		util.LogErrorf("check testbed error: %s", err)
		return "", false
	}

	return resp.Message, resp.Status == model.TestbedFailed
}

func (t *Testbed) Check(testbed string) (*response.CheckTestbedResponse, error) {
	resp, err := util.Req().
		Host(t.ServerConfig.RmsHost).
		Path("api/v1/testbeds/" + testbed + "/check").
		Bearer(t.ServerConfig.Token).
		Get()
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

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

	switch resp.StatusCode {
	case http.StatusOK:
		testbedResponse := &response.CheckTestbedResponse{}
		if err := json.Unmarshal(body, testbedResponse); err != nil {
			return nil, err
		}
		return testbedResponse, nil
	case http.StatusNotFound:
		util.LogInfof("the testbed %s doesn't exist", testbed)
		return nil, nil
	}

	statusErr := &errors.StatusError{}
	if err := json.Unmarshal(body, statusErr); err != nil {
		return nil, err
	}
	return nil, statusErr
}

func (t *Testbed) Auth(testbed string) error {
	resp, err := util.Req().
		Host(t.ServerConfig.RmsHost).
		Path("api/v1/testbeds/" + testbed + "/auth").
		Bearer(t.ServerConfig.Token).
		Put()
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	switch resp.StatusCode {
	case http.StatusOK:
		return nil
	case http.StatusNotFound:
		util.LogInfof("the testbed %s doesn't exist", testbed)
		return nil
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	statusErr := &errors.StatusError{}
	if err := json.Unmarshal(body, statusErr); err != nil {
		return err
	}
	return statusErr
}

func (t *Testbed) RestoreMonitor(testbedName string, prometheusTarballURL string, clusterType string) (*response.RestoreMonitorResponse, error) {
	resp, err := util.Req().
		Host(t.ServerConfig.RmsHost).
		Path("api/v1/testbeds/" + testbedName + "/restore-monitor").
		Json(request.RestoreMonitorRequest{Testbed: testbedName, PrometheusTarballURL: prometheusTarballURL, ClusterType: clusterType}).
		Bearer(t.ServerConfig.Token).
		Post()
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

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

	switch resp.StatusCode {
	case http.StatusOK:
		res := &response.RestoreMonitorResponse{}
		if err := json.Unmarshal(body, res); err != nil {
			return nil, err
		}
		return res, nil
	}

	statusErr := &errors.StatusError{}
	if err := json.Unmarshal(body, statusErr); err != nil {
		return nil, err
	}
	return nil, statusErr
}

func (t *Testbed) Delete(testbed string) error {
	r, err := util.Req().
		Host(t.ServerConfig.RmsHost).
		Path("api/v1/testbeds/" + testbed).
		Bearer(t.ServerConfig.Token).
		Delete()
	if err != nil {
		return err
	}
	b, err := io.ReadAll(r.Body)
	if err != nil {
		return err
	}
	switch r.StatusCode {
	case http.StatusOK:
		util.LogInfof("deleting the testbed %s on the background", testbed)
		return nil
	case http.StatusNotFound:
		util.LogInfof("the testbed %s doesn't exist", testbed)
		return nil
	}
	statusErr := &errors.StatusError{}
	if err := json.Unmarshal(b, statusErr); err != nil {
		return err
	}
	return pkgErrors.Wrapf(statusErr, "delete the testbed %s error", testbed)
}

func (t *Testbed) Complete(testbed string, completionType request.OnTestbedCompletionType) error {
	client := &http.Client{
		Timeout: 20 * time.Minute,
	}
	if !util.IsInK8sPodEnvironment() {
		// get proxy to visit sdkserver
		proxies, err := t.GetProxies()
		if err != nil {
			return err
		}
		proxyURL, err := url.Parse(proxies["HTTP_PROXY"])
		if err != nil {
			return err
		}
		// NOTE: visit in-cluster URL using http proxy
		client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}
	}
	r, err := util.Req().
		Host(fmt.Sprintf("http://sdkserver-peer.%s.svc:8000", testbed)).
		Path("api/v1/testbed" + "/complete?type=" + string(completionType)).
		Bearer(t.ServerConfig.Token).
		// complete maybe slow
		Client(client).
		Post()
	if err != nil {
		return err
	}
	b, err := io.ReadAll(r.Body)
	if err != nil {
		return err
	}
	switch r.StatusCode {
	case http.StatusOK:
		util.LogInfof("complete the testbed %s succeed", testbed)
		return nil
	case http.StatusNotFound:
		util.LogInfof("the testbed %s doesn't exist", testbed)
		return nil
	}
	statusErr := &errors.StatusError{}
	if err := json.Unmarshal(b, statusErr); err != nil {
		return err
	}
	util.LogErrorf("complete the testbed %s error: %s", testbed, statusErr.Error())
	return nil
}

func (t *Testbed) RenewalLease(testbed, renewLease string) error {
	r, err := util.Req().
		Host(t.ServerConfig.RmsHost).
		Path("api/v1/testbeds/" + testbed + "/lease/renew?lease=" + renewLease).
		Bearer(t.ServerConfig.Token).
		Put()
	if err != nil {
		return err
	}
	b, err := io.ReadAll(r.Body)
	if err != nil {
		return err
	}
	switch r.StatusCode {
	case http.StatusOK:
		resp := &response.RenewalLeaseResponse{}
		if err := json.Unmarshal(b, resp); err != nil {
			return err
		}
		util.LogInfof("renew the testbed %s succeed, expire date: %s", testbed, resp.ExpireDate)
		return nil
	case http.StatusNotFound:
		util.LogInfof("the testbed %s doesn't exist", testbed)
		return nil
	}
	statusErr := &errors.StatusError{}
	if err := json.Unmarshal(b, statusErr); err != nil {
		return err
	}
	return pkgErrors.Wrapf(statusErr, "renewal the testbed %s error", testbed)
}

func (t *Testbed) ListTestbed() error {
	r, err := util.Req().
		Host(t.ServerConfig.RmsHost).
		Path("api/v1/testbeds/list").
		Bearer(t.ServerConfig.Token).
		Get()
	if err != nil {
		return err
	}
	b, err := io.ReadAll(r.Body)
	if err != nil {
		return err
	}
	switch r.StatusCode {
	case http.StatusOK:
		resp := &response.ListTestbedResponse{}
		if err := json.Unmarshal(b, resp); err != nil {
			return err
		}
		t := tabby.New()
		t.AddHeader("NAME", "LEASE", "EXPIRE DATE")
		for _, testbed := range resp.Items {
			lease, expire := "", ""
			if testbed.Lease != nil {
				lease = testbed.Lease.Lease
				expire = testbed.Lease.ExpireDate
			}
			t.AddLine(testbed.Name, lease, expire)
		}
		t.Print()
		return nil
	}
	statusErr := &errors.StatusError{}
	if err := json.Unmarshal(b, statusErr); err != nil {
		return err
	}
	return pkgErrors.Wrapf(statusErr, "list testbeds error")
}

func (t *Testbed) WaitForReady(testbed string, interval, timeout time.Duration) (*response.GetTestbedResponse, error) {
	var response *response.GetTestbedResponse
	err := wait.PollImmediate(interval, timeout, func() (done bool, err error) {
		response, err = t.Get(testbed)
		if err != nil {
			util.LogErrorf("get testbed error: %s", err)
			return false, nil
		}
		ready := true
		for _, item := range response.Items {
			if item.Ready == nil || *item.Ready == false {
				ready = false
				util.LogInfof("resource is not ready: %s/%s", item.Testbed, item.Name)
			}
			if item.Ready != nil && *item.Ready == true {
				util.LogInfof("resource is ready: %s/%s", item.Testbed, item.Name)
			}
		}

		if !ready {
			failedMessage, failed := t.testbedFailed(testbed)
			if failed {
				return ready, pkgErrors.WithMessage(util.ErrTestbedConfig, failedMessage)
			}
		}

		return ready, nil
	})
	if err != nil {
		if err == wait.ErrWaitTimeout {
			err = pkgErrors.WithMessage(util.ErrTimeout, err.Error())
		}
		return nil, err
	}

	return response, nil
}

func (t *Testbed) GetProxies() (map[string]string, error) {
	client := http.Client{Timeout: time.Second * 10}
	proxies := make(map[string]string)
	_, err := util.Req().
		Host(t.ServerConfig.RmsHost).
		Path("api/v1/proxy").
		Client(&client).
		Bearer(t.ServerConfig.Token).
		Get(&proxies)

	return proxies, err
}

func (t *Testbed) GetLogFromMinio(testbed string, getAll bool) error {
	testbedLogLinksResponse := &response.GetLogLinksResponse{}

	_, err := util.Req().
		Host(t.ServerConfig.RmsHost).
		Path("api/v1/testbeds/" + testbed + "/log_links").
		Bearer(t.ServerConfig.Token).
		Into(testbedLogLinksResponse).
		Get()
	if err != nil {
		return err
	}

	for path, link := range testbedLogLinksResponse.LogLinks {
		if getAll || util.AskForConfirmation("download "+path, true) {
			util.LogInfof("downloading %s", path)
			if err := util.DownloadFile(path, link); err != nil {
				return err
			}
		}
	}

	return nil
}

func (t *Testbed) GenerateCredential(testbedName string, output string) error {
	key, err := genPrivateKey()
	if err != nil {
		return err
	}
	outfile, err := os.Create(output)
	if err != nil {
		return err
	}
	defer outfile.Close()
	err = genCredentialFromRMS(t.ServerConfig.RmsHost, t.ServerConfig.Token, testbedName, key, outfile)
	return err
}

func genPrivateKey() ([]byte, error) {
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return nil, err
	}
	privateKey, err := x509.MarshalPKCS8PrivateKey(key)
	if err != nil {
		return nil, err
	}
	pk := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKey})
	return pk, nil
}

func genCredentialFromRMS(host string, token string, testbed string, pk []byte, output io.Writer) error {
	resp := response.CredentialResponse{}
	_, err := util.Req().
		Host(host).
		Client(&http.Client{Timeout: 30 * time.Second}).
		Path("api/v1/credential").
		Bearer(token).
		Json(request.CredentialRequest{PEM: pk, TestbedName: testbed}).
		Post(&resp)
	if err != nil {
		return err
	}
	if resp.Status == "OK" {
		_, err = output.Write(resp.Credential)
	}
	return err
}
