package resources

import (
	"fmt"
	urllib "net/url"
	"time"

	sdkresource "github.com/pingcap/test-infra/sdk/resource"
	"github.com/pingcap/test-infra/services/sdkserver/config"
	"github.com/pingcap/test-infra/services/sdkserver/core"
	"github.com/pingcap/test-infra/services/sdkserver/diag"
	"github.com/pingcap/test-infra/services/sdkserver/diag/client/operations"
	"github.com/pingcap/test-infra/services/sdkserver/diag/models"
	"github.com/pingcap/test-infra/services/sdkserver/utils"
)

type TidbCluster struct {
	TestResource
}

var _ core.Archivable = TidbCluster{}

// Archive implements the core.Archivable interface
func (t TidbCluster) Archive() (map[string]string, error) {
	// 1. trigger collect
	collectResp, err := diag.DefaultClient.Operations.CollectData(
		operations.NewCollectDataParams().WithBody(&models.CollectJobRequest{
			ClusterName: t.sdkResource.Handle().Name,
			Namespace:   t.sdkResource.Handle().Testbed,
			Collectors:  []string{diag.CollectTypeDebug, diag.CollectTypeMonitor, diag.CollectTypeConfig}, // see: https://clinic-docs.vercel.app/docs/getting-started/%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%E8%8C%83%E5%9B%B4%E8%AF%B4%E6%98%8E/clinic-data-instruction-for-operator
			From:        config.DefaultConfig.DiagCollectStartTime,
			To:          time.Now().Format(time.RFC3339),
		}))
	if err != nil {
		return nil, err
	}

	// 2. poll collect progress until success
	err = utils.PollImmediate(5*time.Second, 30*time.Minute, func() (done bool, err error) {
		collectStatus, err := diag.DefaultClient.Operations.GetCollectJob(
			operations.NewGetCollectJobParams().WithID(collectResp.Payload.ID))
		if err != nil {
			return false, err
		}

		if diag.IsStatusError(collectStatus.Payload.Status) {
			return false, fmt.Errorf("%s/%s diag collect, id: %s, status: %s", t.sdkResource.Handle().Testbed, t.sdkResource.Handle().Name, collectStatus.Payload.ID, collectStatus.Payload.Status)
		}

		return diag.IsStatusSuccess(collectStatus.Payload.Status), nil
	})
	if err != nil {
		return nil, err
	}

	// 3. trigger upload
	_, err = diag.DefaultClient.Operations.UploadDataSet(operations.NewUploadDataSetParams().WithID(collectResp.Payload.ID))
	if err != nil {
		return nil, err
	}

	// 4. poll collect progress
	// TODO: if upload takes too long we need optimization like skip polling to avoid waste of testbed resource
	// currently doesn't return resultURL until upload is done
	var resultURL string
	err = utils.PollImmediate(5*time.Second, 30*time.Minute, func() (done bool, err error) {
		uploadStatus, _, err := diag.DefaultClient.Operations.GetUploadTask( // second return value "NoContent" can never exist according to diag's code
			operations.NewGetUploadTaskParams().WithID(collectResp.Payload.ID))
		if err != nil {
			return false, err
		}

		if diag.IsStatusError(uploadStatus.Payload.Status) {
			return false, fmt.Errorf("%s/%s diag upload, id: %s, status: %s", t.sdkResource.Handle().Testbed, t.sdkResource.Handle().Name, uploadStatus.Payload.ID, uploadStatus.Payload.Status)
		}

		if diag.IsStatusSuccess(uploadStatus.Payload.Status) {
			resultURL = uploadStatus.Payload.Result
			return true, nil
		}

		return false, nil
	})
	if err != nil {
		return nil, err
	}

	return map[string]string{t.sdkResource.Handle().Name: resultURL}, nil
}

func (t TidbCluster) ListEntry() ([]core.DirEntry, error) {
	s, err := t.TestResource.ListEntry()
	if err != nil {
		return s, err
	}
	return append(s,
		core.DirEntry{Name: "tidb", Resource: NewTidb(&t)},
		core.DirEntry{Name: "tikv", Resource: NewTikv(&t)},
	), nil
}

func NewTidbCluster(tc sdkresource.TiDBCluster) TidbCluster {
	t := TidbCluster{
		TestResource: NewTestResource(tc),
	}
	t.Init(&t)
	return t
}

type Tidb struct {
	core.Directory
	parent *TidbCluster
}

func (t Tidb) ListEntry() ([]core.DirEntry, error) {
	tc := t.parent.sdkResource.(sdkresource.TiDBCluster)
	return []core.DirEntry{
		{Name: "dsn", ResourceFunc: func() (core.Resource, error) {
			url, err := tc.ServiceURL(sdkresource.DBAddr)
			if err != nil {
				return nil, core.NewNotAvailableError(err)
			}
			return core.NewStringFile(url2dsn(*url)), nil
		}},
		{Name: "host", ResourceFunc: func() (core.Resource, error) {
			url, err := tc.ServiceURL(sdkresource.DBAddr)
			if err != nil {
				return nil, core.NewNotAvailableError(err)
			}
			return core.NewStringFile(url.Hostname()), nil
		}},
		{Name: "port", ResourceFunc: func() (core.Resource, error) {
			url, err := tc.ServiceURL(sdkresource.DBAddr)
			if err != nil {
				return nil, core.NewNotAvailableError(err)
			}
			return core.NewStringFile(url.Port()), nil
		}},
		{Name: "username", ResourceFunc: func() (core.Resource, error) {
			url, err := tc.ServiceURL(sdkresource.DBAddr)
			if err != nil {
				return nil, core.NewNotAvailableError(err)
			}
			return core.NewStringFile(url.User.Username()), nil
		}},
		{Name: "password", ResourceFunc: func() (core.Resource, error) {
			url, err := tc.ServiceURL(sdkresource.DBAddr)
			if err != nil {
				return nil, core.NewNotAvailableError(err)
			}
			password, _ := url.User.Password()
			return core.NewStringFile(password), nil
		}},
		{Name: "spec", ResourceFunc: func() (core.Resource, error) {
			spec, err := tc.GetSpec(sdkresource.TiDB)
			if err != nil {
				return nil, core.NewNotAvailableError(err)
			}
			return core.NewJSON(spec), nil
		}},
	}, nil
}

func url2dsn(url urllib.URL) string {
	password, _ := url.User.Password()
	return fmt.Sprintf("%s:%s@tcp(%s:%s)/",
		url.User.Username(),
		password,
		url.Hostname(),
		url.Port(),
	)
}

func NewTidb(parent *TidbCluster) Tidb {
	t := Tidb{parent: parent}
	t.Init(&t)
	return t
}

type Tikv struct {
	core.Directory
	parent *TidbCluster
}

func (t Tikv) ListEntry() ([]core.DirEntry, error) {
	tc := t.parent.sdkResource.(sdkresource.TiDBCluster)
	return []core.DirEntry{
		{Name: "spec", ResourceFunc: func() (core.Resource, error) {
			spec, err := tc.GetSpec(sdkresource.TiKV)
			if err != nil {
				return nil, core.NewNotAvailableError(err)
			}
			return core.NewJSON(spec), nil
		}},
	}, nil
}

func NewTikv(parent *TidbCluster) Tikv {
	t := Tikv{parent: parent}
	t.Init(&t)
	return t
}
