package driver

import (
	"encoding/base64"
	"fmt"
	"net/url"
	"os"
	"path"
	"strings"
	"time"

	"github.com/avast/retry-go/v4"
	httptransport "github.com/go-openapi/runtime/client"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	dpmgrclient "github.com/tidbcloud/infra-provider/regional-server/test/api/dataplane-manager/client"
	"github.com/tidbcloud/infra-provider/regional-server/test/api/dataplane-manager/client/data_plane_service"
	"github.com/tidbcloud/infra-provider/regional-server/test/api/dataplane-manager/models"
	"gopkg.in/yaml.v3"
	"k8s.io/client-go/tools/clientcmd"
)

const (
	// Environment variable for logical region
	// if set, it will only run tests in this logical region
	ENV_LOGICAL_REGION = "LOGICAL_REGION"
	// Environment variable for data plane ID
	// if set, it will use this data plane ID instead of creating a new one
	ENV_DATA_PLANE_ID = "DATA_PLANE_ID"
)

type DataPlaneDriver struct {
	dataPlaneClient *dpmgrclient.DataplaneManager
	dataPlaneID     string
	config          *RegionConfig
}

func (d *DataPlaneDriver) Setup() error {
	if err := d.initConfig(); err != nil {
		return err
	}
	if err := d.initClient(); err != nil {
		return err
	}

	fmt.Println("start to create dataplane")
	dataPlaneID, err := d.createDataPlane()
	d.dataPlaneID = dataPlaneID
	if err != nil {
		return err
	}

	// NOTE: for BYOC, setup a DataPlane need more that 30 minutes
	if err := retryDo("waitDataPlaneCreated", d.checkDataPlaneCreated, 120, time.Second*30); err != nil {
		return err
	}

	fmt.Println("dataplane is created", "id", d.dataPlaneID)
	return nil
}

func (d *DataPlaneDriver) TearDown() error {
	if d.config.ExistingDataPlaneID != "" {
		return nil
	}

	fmt.Println("start to delete dataplane")
	if err := d.deleteDataPlane(); err != nil {
		return err
	}

	if err := retryDo("waitDataPlaneDeleted", d.checkDataPlaneDeleted, 60, time.Second*30); err != nil {
		return err
	}

	fmt.Println("dataplane is deleted", "id", d.dataPlaneID)
	return nil
}

func (d *DataPlaneDriver) GetDataPlaneID() string {
	return d.dataPlaneID
}

func (d *DataPlaneDriver) GetClient() *dpmgrclient.DataplaneManager {
	return d.dataPlaneClient
}

func (d *DataPlaneDriver) GetConfig() *RegionConfig {
	return d.config
}

func (d *DataPlaneDriver) initConfig() error {
	conf := &Config{}
	data, err := os.ReadFile("./config.yaml")
	if err != nil {
		return err
	}
	if err := yaml.Unmarshal(data, conf); err != nil {
		return err
	}
	if len(conf.Regions) == 0 {
		return fmt.Errorf("region config is empty")
	}
	logicalRegion := os.Getenv(ENV_LOGICAL_REGION)
	if logicalRegion != "" {
		for _, region := range conf.Regions {
			if region.LogicalRegion == logicalRegion {
				d.config = region
				break
			}
		}
		if d.config == nil {
			return fmt.Errorf("logical region %s not found in config", logicalRegion)
		}
	} else {
		d.config = conf.Regions[0] // use the first region if LogicalRegion is not set
	}

	// Override ExistingDataPlaneID if DATA_PLANE_ID environment variable is set
	if dataPlaneID := os.Getenv(ENV_DATA_PLANE_ID); dataPlaneID != "" {
		d.config.ExistingDataPlaneID = dataPlaneID
	}

	return nil
}

func (d *DataPlaneDriver) initClient() error {
	u, err := url.Parse(d.config.EndpointURL)
	if err != nil {
		return fmt.Errorf("parse endpoint %s error: %w", d.config.EndpointURL, err)
	}
	dataPlaneBasePath := dpmgrclient.DefaultBasePath
	if u.Path != "" {
		dataPlaneBasePath = path.Join(u.Path, dpmgrclient.DefaultBasePath)
	}
	transport := httptransport.New(u.Host, dataPlaneBasePath, []string{u.Scheme})
	d.dataPlaneClient = dpmgrclient.New(transport, nil)
	return nil
}

func (d *DataPlaneDriver) createDataPlane() (string, error) {
	if d.config.ExistingDataPlaneID != "" {
		fmt.Println("use existing dataplane", "id", d.config.ExistingDataPlaneID)
		return d.config.ExistingDataPlaneID, nil
	}

	var dataPlaneID = d.genDataPlaneID()
	createParams := &data_plane_service.DataPlaneServiceCreateDataPlaneParams{
		DataPlaneID: dataPlaneID,
		DataPlane: &models.V1DataPlane{
			AccountID: d.config.CloudAccountID,
			Tags: map[string]string{
				"tags.tidbcloud.com/test":       "true",
				"tags.tidbcloud.com/created-by": "e2e",
				consts.TenantLabelKey:           "123456",
				consts.DataplaneLabelKey:        dataPlaneID,
			},
			Vpc: &models.DataPlaneVpc{
				Cidr: "172.16.0.0/21",
			},
		},
	}

	err := retryDo("createDataPlane", func() error {
		resp, err := d.dataPlaneClient.DataPlaneService.DataPlaneServiceCreateDataPlane(createParams)
		if err != nil {
			return err
		}
		dataPlaneID = resp.Payload.ID
		return nil
	}, 3, time.Second*10)
	return dataPlaneID, err
}

func (d *DataPlaneDriver) checkDataPlaneCreated() error {
	getParams := &data_plane_service.DataPlaneServiceGetDataPlaneParams{
		DataPlaneID: d.dataPlaneID,
	}
	resp, err := d.dataPlaneClient.DataPlaneService.DataPlaneServiceGetDataPlane(getParams)
	if err != nil {
		return err
	}
	if resp.Payload.Status != models.V1DataPlaneStatusActive {
		return fmt.Errorf("dataplane %s is %s, not active", d.dataPlaneID, resp.Payload.Status)
	}
	if resp.Payload.Kubernetes == nil || resp.Payload.Kubernetes.Kubeconfig == "" {
		return fmt.Errorf("dataplane %s kubeconfig is empty", d.dataPlaneID)
	}
	// kubeconfig is base64 encoded, decode and check it's valid
	kubeconfig, err := base64.StdEncoding.DecodeString(resp.Payload.Kubernetes.Kubeconfig)
	if err != nil {
		return fmt.Errorf("dataplane %s kubeconfig can't be decoded: %w", d.dataPlaneID, err)
	}
	cfg, err := clientcmd.Load(kubeconfig)
	if err != nil {
		return fmt.Errorf("dataplane %s kubeconfig is invalid: %w", d.dataPlaneID, err)
	}
	if len(cfg.Clusters) == 0 {
		return fmt.Errorf("dataplane %s kubeconfig has no clusters", d.dataPlaneID)
	}
	ctxName := cfg.CurrentContext
	if cfg.Clusters[ctxName].Server == "" {
		return fmt.Errorf("dataplane %s kubeconfig cluster server is empty", d.dataPlaneID)
	}

	if resp.Payload.APIGateway == "" {
		return fmt.Errorf("dataplane %s api gateway is empty", d.dataPlaneID)
	}
	if d.config.CloudProvider != "azure" && resp.Payload.AuthToken == "" {
		return fmt.Errorf("dataplane %s auth token is empty", d.dataPlaneID)
	}
	if resp.Payload.AccountID == "" {
		return fmt.Errorf("dataplane %s account id is empty", d.dataPlaneID)
	}
	if resp.Payload.NodeRole == "" {
		return fmt.Errorf("dataplane %s node role is empty", d.dataPlaneID)
	}

	return nil
}

func (d *DataPlaneDriver) deleteDataPlane() error {
	deleteParams := &data_plane_service.DataPlaneServiceDeleteDataPlaneParams{
		DataPlaneID: d.dataPlaneID,
	}
	return retryDo("deleteDataPlane", func() error {
		_, err := d.dataPlaneClient.DataPlaneService.DataPlaneServiceDeleteDataPlane(deleteParams)
		return err
	}, 3, time.Second*10)
}

func (d *DataPlaneDriver) checkDataPlaneDeleted() error {
	getParams := &data_plane_service.DataPlaneServiceGetDataPlaneParams{
		DataPlaneID: d.dataPlaneID,
	}
	_, err := d.dataPlaneClient.DataPlaneService.DataPlaneServiceGetDataPlane(getParams)
	if err == nil {
		return fmt.Errorf("dataplane %s is not deleted", d.dataPlaneID)
	}

	if strings.Contains(err.Error(), "404") {
		fmt.Println("get dataplane deleted error", "err", err)
		return nil
	}
	return err
}

func (d *DataPlaneDriver) genDataPlaneID() string {
	return fmt.Sprintf("e2e-test-%d", time.Now().Unix())
}

func retryDo(name string, fun func() error, attempts uint, delay time.Duration) error {
	opt := []retry.Option{
		retry.DelayType(retry.FixedDelay),
		retry.Attempts(attempts),
		retry.Delay(delay),
		retry.LastErrorOnly(true),
		retry.OnRetry(func(n uint, err error) {
			fmt.Println(name, "attempt", n, "error", err)
		}),
	}
	return retry.Do(fun, opt...)
}
