package revision

import (
	"context"
	"fmt"
	"hash"
	"hash/fnv"
	"reflect"
	"strconv"
	"time"

	"github.com/davecgh/go-spew/spew"
	"github.com/pingcap/errors"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type Interface interface {
	ListConfigRevisions(selector labels.Selector) ([]cloudv1.ConfigRevision, error)
	// CreateConfigRevision attempts to create the revision. If name collision occurs,
	// a unique identifier is added to the hash of the revision and renamed.
	// If the returned error is not nil, creation failed.
	// If the returned error is nil, the returned ConfigRevision has been created.
	CreateConfigRevision(revision *cloudv1.ConfigRevision) (*cloudv1.ConfigRevision, error)

	GetConfigRevisions(name types.NamespacedName) (*cloudv1.ConfigRevision, error)

	//RenewConfigRevision(revision *cloudv1.ConfigRevision) error

	DeleteConfigRevision(revision *cloudv1.ConfigRevision) error
}

func NewRevisioner(cli client.Client) Interface {
	return &revisioner{
		client: cli,
	}
}

type revisioner struct {
	client client.Client
}

func (r *revisioner) ListConfigRevisions(selector labels.Selector) ([]cloudv1.ConfigRevision, error) {
	crs := &cloudv1.ConfigRevisionList{}
	err := r.client.List(context.TODO(), crs, &client.ListOptions{
		LabelSelector: selector,
	})
	if err != nil {
		return nil, errors.Annotatef(err, "list configRevisions with selector %s", selector.String())
	}
	return crs.Items, nil
}

func (r *revisioner) GetConfigRevisions(name types.NamespacedName) (*cloudv1.ConfigRevision, error) {
	cr := &cloudv1.ConfigRevision{}
	err := r.client.Get(context.TODO(), name, cr)
	if err != nil {
		return nil, errors.Annotatef(err, "get configRevisions with name %s", name.Name)
	}
	return cr, nil
}

func (r *revisioner) CreateConfigRevision(revision *cloudv1.ConfigRevision) (*cloudv1.ConfigRevision, error) {
	err := r.client.Create(context.TODO(), revision)
	return revision, err

	//clone := revision.DeepCopy()
	//// Continue to attempt to create the revision updating the name with a new hash on each iteration
	//probe := int64(0)
	//for {
	//	var hash string
	//	if probe > 0 {
	//		hash = HashConfigRevision(revision, &probe)
	//	} else {
	//		hash = HashConfigRevision(revision, nil)
	//	}
	//	index := strings.LastIndex(revision.Name, "-")
	//	clone.Name = revision.Name[0:index+1] + hash
	//	clone.Labels[cloudv1.ConfigRevisionHashLabel] = hash
	//	err := r.client.Create(context.TODO(), clone)
	//	if apierrors.IsAlreadyExists(err) {
	//		exists := &cloudv1.ConfigRevision{}
	//		err := r.client.Get(context.TODO(), client.ObjectKey{Name: clone.Name}, exists)
	//		if err != nil {
	//			return nil, err
	//		}
	//		if reflect.DeepEqual(exists.Spec, clone.Spec) {
	//			return exists, nil
	//		}
	//		probe++
	//		continue
	//	}
	//	return clone, err
	//}
}

//func (r *revisioner) RenewConfigRevision(revision *cloudv1.ConfigRevision) error {
//	patch := client.MergeFrom(revision.DeepCopy())
//	if revision.Annotations == nil {
//		revision.Annotations = make(map[string]string)
//	}
//	revision.Annotations[cloudv1.ConfigRevisionActiveAnnotation] = time.Now().Format(time.RFC3339)
//	return r.client.Patch(context.TODO(), revision, patch)
//}

func (r *revisioner) DeleteConfigRevision(revision *cloudv1.ConfigRevision) error {
	return r.client.Delete(context.TODO(), revision)
}

// NewConfigRevision will take a snapshot of ClusterConfig as ConfigRevision
// and set its name to the concatenation of layers tuple and hash.
func NewConfigRevision(cc *cloudv1.ClusterConfig) *cloudv1.ConfigRevision {
	clone := cc.DeepCopy()
	cr := &cloudv1.ConfigRevision{
		ObjectMeta: metav1.ObjectMeta{
			Labels: clone.Labels,
		},
		Spec: cloudv1.ConfigRevisionSpec{
			Version:        clone.Spec.Version,
			TiDB:           clone.Spec.TiDB,
			PD:             clone.Spec.PD,
			TiKV:           clone.Spec.TiKV,
			TiFlash:        clone.Spec.TiFlash,
			TiProxy:        clone.Spec.TiProxy,
			TiFlashLearner: clone.Spec.TiFlashLearner,
			Backup:         clone.Spec.Backup,
			Restore:        clone.Spec.Restore,
			Lightning:      clone.Spec.Lightning,
			TiCDC:          clone.Spec.TiCDC,
		},
	}
	if cr.GetLabels() == nil {
		cr.ObjectMeta.Labels = make(map[string]string)
	}
	digest := HashConfigRevision(cr, "")
	cr.Name = GenConfigRevisionName(cc, digest)
	cr.Labels[cloudv1.ConfigRevisionHashLabel] = digest
	if cr.Annotations == nil {
		cr.Annotations = make(map[string]string)
	}
	cr.Annotations[cloudv1.ConfigRevisionActiveAnnotation] = time.Now().Format(time.RFC3339)
	return cr
}

func RectifyConfigRevisionForCreate(cluster *cloudv1.Cluster, cr *cloudv1.ConfigRevision, idempotencyKey string) {
	cloned := cr.DeepCopy()

	cr.ObjectMeta = metav1.ObjectMeta{
		Name:        cloned.Name,
		Namespace:   cloned.Namespace,
		Labels:      cloned.Labels,
		Annotations: cloned.Annotations,
	}
	if cr.GetLabels() == nil {
		cr.Labels = make(map[string]string)
	}
	cr.Labels[cloudv1.TenantTagKey] = cluster.GetTenantID()
	cr.Labels[cloudv1.ProjectTagKey] = cluster.GetProjectID()
	cr.Labels[cloudv1.ClusterTagKey] = cluster.GetName()

	now := time.Now()
	digest := HashConfigRevision(cr, idempotencyKey)
	cr.Name = GenConfigRevisionNameV2(cr, digest)
	cr.Labels[cloudv1.ConfigRevisionHashLabel] = digest
	cr.Labels[cloudv1.ConfigRevisionIdempotencyKey] = idempotencyKey
	if cr.Annotations == nil {
		cr.Annotations = make(map[string]string)
	}
	cr.Annotations[cloudv1.ConfigRevisionActiveAnnotation] = now.Format(time.RFC3339)

	cr.Status = cloudv1.ConfigRevisionStatus{}
}

func HashConfigRevision(revision *cloudv1.ConfigRevision, salt string) string {
	hf := fnv.New32()
	DeepHashObject(hf, revision.Spec)

	if salt != "" {
		hf.Write([]byte(salt))
	}
	return fmt.Sprintf("%x", hf.Sum32())
}

func EqualRevision(lhs *cloudv1.ConfigRevision, rhs *cloudv1.ConfigRevision) bool {
	var lhsHash, rhsHash *uint32
	if lhs == nil || rhs == nil {
		return lhs == rhs
	}
	if hs, found := lhs.Labels[cloudv1.ConfigRevisionHashLabel]; found {
		hash, err := strconv.ParseInt(hs, 10, 32)
		if err == nil {
			lhsHash = new(uint32)
			*lhsHash = uint32(hash)
		}
	}
	if hs, found := rhs.Labels[cloudv1.ConfigRevisionHashLabel]; found {
		hash, err := strconv.ParseInt(hs, 10, 32)
		if err == nil {
			rhsHash = new(uint32)
			*rhsHash = uint32(hash)
		}
	}
	if lhsHash != nil && rhsHash != nil && *lhsHash != *rhsHash {
		return false
	}
	return reflect.DeepEqual(lhs.Spec, rhs.Spec)
}

// DeepHashObject is copied from "infra-api/pkg/util/util.go"
func DeepHashObject(hasher hash.Hash, objectToWrite interface{}) {
	hasher.Reset()
	printer := spew.ConfigState{
		Indent:         " ",
		SortKeys:       true,
		DisableMethods: true,
		SpewKeys:       true,
	}
	printer.Fprintf(hasher, "%#v", objectToWrite)
}

func getShortCommitID(commitID string) string {
	if len(commitID) > 7 {
		return commitID[:7]
	}
	return commitID
}

func GenConfigRevisionName(cc *cloudv1.ClusterConfig, digest string) string {
	if cc.Labels == nil {
		cc.Labels = make(map[string]string)
	}

	prefix := "base" + cc.Labels[cloudv1.VersionLabel]
	if t, ok := cc.Labels[cloudv1.TenantTagKey]; ok {
		prefix = fmt.Sprintf("%s-t%s", prefix, t)
	}
	if c, ok := cc.Labels[cloudv1.ClusterTagKey]; ok {
		prefix = fmt.Sprintf("%s-c%s", prefix, c)
	}
	shortCommitID := getShortCommitID(cc.Spec.ConfigMeta.GitCommitID)
	return fmt.Sprintf("%s-%s-%s", prefix, shortCommitID, digest)
}

// GenConfigRevisionNameV2 is the same as GenConfigRevisionName, but with new prefix designing for 3-layers arch
func GenConfigRevisionNameV2(cr *cloudv1.ConfigRevision, digest string) string {
	if cr.Labels == nil {
		cr.Labels = make(map[string]string)
	}
	prefix := "cr-" + cr.Labels[cloudv1.VersionLabel]
	prefix = fmt.Sprintf("%s-t%s-c%s", prefix, cr.Labels[cloudv1.TenantTagKey], cr.Labels[cloudv1.ClusterTagKey])
	return fmt.Sprintf("%s-%s", prefix, digest)
}
