package k8s

import (
	"archive/tar"
	"context"
	"database/sql"
	"fmt"
	"hash/fnv"
	"io"
	"net/url"
	"os"
	"strings"
	"time"

	"github.com/pingcap/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"

	"github.com/pingcap/test-infra/sdk/resource"

	apiresource "github.com/pingcap/test-infra/common/model/resource"
)

const (
	TestInfraName = "test-infra/name"
)

// LoadConfig loads a rest.Config
func LoadConfig(kubeConfig string) (config *rest.Config, err error) {
	config, err = clientcmd.BuildConfigFromFlags("", kubeConfig)
	if err != nil {
		return nil, err
	}
	return
}

// FIXME(@mahjonp): check exists & replace with the testbed CR object in future
func GetOwnerReferenceFromTestbed(k *KubeClientSet, testbed string) (*metav1.OwnerReference, error) {
	testCli := NewTestbedClient(k)
	resource, err := testCli.Get(apiresource.ResourceHandle{
		Name: testbed,
		Type: apiresource.ResourceTypeTestbed,
	})
	if err != nil {
		return nil, err
	}
	tb := resource.(*Testbed)
	return &metav1.OwnerReference{
		UID:                tb.cm.UID,
		APIVersion:         "v1",
		Kind:               "ConfigMap",
		Name:               tb.cm.Name,
		BlockOwnerDeletion: &[]bool{true}[0],
	}, nil
}

func AddOwnerReference(obj *metav1.ObjectMeta, owner *unstructured.Unstructured) {
	for _, ref := range obj.OwnerReferences {
		if ref.UID == owner.GetUID() {
			return
		}
	}
	obj.OwnerReferences = append(obj.OwnerReferences, metav1.OwnerReference{
		APIVersion:         owner.GetAPIVersion(),
		Kind:               owner.GetKind(),
		Name:               owner.GetName(),
		UID:                owner.GetUID(),
		BlockOwnerDeletion: &[]bool{true}[0],
	})
}

func pingDB(t resource.Addressable) error {
	addr, err := t.ServiceURL(resource.DBAddr)
	if err != nil {
		return err
	}

	dsn := resource.BuildDSN(addr, "")
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return err
	}
	defer db.Close()

	ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
	defer cancel()
	return db.PingContext(ctx)
}

func getDB(t resource.Addressable, dbName string) (*sql.DB, error) {
	addr, err := t.ServiceURL(resource.DBAddr)
	if err != nil {
		return nil, err
	}

	db, err := sql.Open("mysql", resource.BuildDSN(addr, dbName, "timeout=2m"))
	if err != nil {
		return nil, errors.Trace(err)
	}
	if err := db.Ping(); err != nil {
		return nil, errors.Trace(err)
	}
	return db, nil
}

// ref: https://github.com/kubernetes/kubernetes/blob/edb0a72cff0e43bab72a02cada8486d562ee1cd5/staging/src/k8s.io/kubectl/pkg/cmd/cp/cp.go#L484-L549
func untarAll(tarStream io.ReadCloser, archiver resource.ArchiverFunc, node, prefix string) (map[string]*url.URL, error) {
	defer tarStream.Close()

	urlMap := make(map[string]*url.URL)
	tarReader := tar.NewReader(tarStream)
	for {
		header, err := tarReader.Next()
		if err != nil {
			if err != io.EOF {
				return nil, err
			}
			break
		}
		if !strings.HasPrefix(header.Name, prefix) {
			return nil, fmt.Errorf("tar contents corrupted")
		}
		fileNameWithoutPrefix := header.Name[len(prefix):]
		mode := header.FileInfo().Mode()
		// skipping dir
		if header.FileInfo().IsDir() {
			continue
		}
		// skipping symlink
		if mode&os.ModeSymlink != 0 {
			continue
		}
		// add node name as prefix when archive file: tc-pd-0/pd.log
		objectName := fmt.Sprintf("%s/%s", node, fileNameWithoutPrefix)
		url, err := archiver(objectName, tarReader)
		if err != nil {
			return nil, err
		}
		urlMap[objectName] = url
	}
	return urlMap, nil
}

func stringsContains(list []string, target string) bool {
	for _, elem := range list {
		if elem == target {
			return true
		}
	}
	return false
}

func stringsRemove(list []string, target string) []string {
	if len(list) == 0 {
		return list
	}
	newList := make([]string, 0, len(list)-1)
	for _, elem := range list {
		if target != elem {
			newList = append(newList, elem)
		}
	}
	return newList
}

// copied from https://github.com/tidbcloud/config-service/blob/e4d7ca7c8e2e2733b1ad738046c0b7856bcd4f24/gardener/util.go#L48
func TiDBCloudShootName(projectID string, cloudProvider string, region string) string {
	rawName := fmt.Sprintf("%s-%s-%s", projectID, cloudProvider, region)
	return fnvHash(rawName)
}

func fnvHash(raw string) string {
	hash := fnv.New32()
	if _, err := hash.Write([]byte(raw)); err != nil {
		return ""
	}
	return fmt.Sprintf("%x", hash.Sum32())
}
