package common

import (
	"context"
	"fmt"

	certv1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
	v1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
	opv2 "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	certutil "github.com/tidbcloud/infra-provider/pkg/cert"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

type TiDBGroupCert struct {
	*task.BaseTask
}

func NewTiDBGroupCert(ctx *task.Context) *TiDBGroupCert {
	return &TiDBGroupCert{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

func (t *TiDBGroupCert) Name() string {
	return "tidbgroup.Cert"
}

func (t *TiDBGroupCert) useWildcardDNS() bool {
	return t.Cluster.FeatureEnabled(consts.ClusterFeatureWildcardDNS)
}

func (t *TiDBGroupCert) selfSignedClusterComponents() bool {
	return t.Cluster.FeatureEnabled(consts.ClusterFeatureUseSelfSignedCertForTiDBComponents)
}

func (t *TiDBGroupCert) selfSignedTiDBServer() bool {
	return t.Cluster.FeatureEnabled(consts.ClusterFeatureSelfSignedCertForTidbServer)
}

func (t *TiDBGroupCert) shortLivedComponentCert() bool {
	return t.Cluster.FeatureEnabled(consts.ClusterFeatureShortLivedComponentCert)
}

func (t *TiDBGroupCert) componentCerts(cn *namer.Namer) []*certv1.Certificate {
	certs := []*certv1.Certificate{
		// cert for tidb
		certutil.ComponentCert(cn, t.selfSignedClusterComponents()),
		// cert for tiproxy
		certutil.ComponentCert(
			cn.WithComponent(string(cloudv1.ComponentTiProxy)),
			t.selfSignedClusterComponents(),
		),
		// tidb-operator hardcode certificate secret name with tc name as prefix
		// extragroup has different tc name, so even cluster client and tidb component mtls need separate certificates
		certutil.TiDBClientCert(cn, t.shortLivedComponentCert()),
		certutil.ClusterClientCert(cn.WithComponent(""), t.selfSignedClusterComponents()),
	}

	return certs
}

func (t *TiDBGroupCert) tidbServerCerts(cn *namer.Namer) (certs []*certv1.Certificate) {
	certs = append(certs, certutil.TiDBServerCert(cn, t.useWildcardDNS(), t.selfSignedTiDBServer(), t.shortLivedComponentCert()))
	return certs
}

func (t *TiDBGroupCert) Create(ctx context.Context) deploy.Result {
	remote := t.EnvProvider.GetClient()

	errList := []error{}
	for _, g := range t.Cluster.Spec.ExtraGroups {
		if g.Deleted() {
			continue
		}
		cn := t.TidbGroupNamer(&g).
			WithComponent(string(cloudv1.ComponentTiDB))

		var owner *metav1.OwnerReference
		if t.Cluster.FeatureEnabled(consts.ClusterFeatureUseTiDBOperatorV2) {
			dbg := &opv2.TiDBGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name:      cn.GetTiDBName(),
					Namespace: cn.GetTiDBNamespace(),
				},
			}
			if err := remote.Get(ctx, client.ObjectKeyFromObject(dbg), dbg); err != nil {
				errList = append(errList, err)
				continue
			}
			owner = metav1.NewControllerRef(dbg, opv2.SchemeGroupVersion.WithKind("TiDBGroup"))
		} else {
			// get owner reference
			tc := &opv1alpha1.TidbCluster{
				ObjectMeta: metav1.ObjectMeta{
					Name:      cn.GetTiDBName(),
					Namespace: cn.GetTiDBNamespace(),
				},
			}

			if err := remote.Get(ctx, client.ObjectKeyFromObject(tc), tc); err != nil {
				errList = append(errList, err)
				continue
			}
			owner = metav1.NewControllerRef(tc, opv1alpha1.SchemeGroupVersion.WithKind("TidbCluster"))
		}

		certs := []*certv1.Certificate{}
		certs = append(certs, t.componentCerts(cn)...)
		tidbServerCerts, err := t.genOrDeleteTiDBServerCerts(ctx, remote, g, cn)
		if err != nil {
			return deploy.Error(err)
		}
		certs = append(certs, tidbServerCerts...)

		for _, cert := range certs {
			cert.OwnerReferences = append(cert.OwnerReferences, *owner)
			if _, err := controllerutil.CreateOrUpdate(ctx, remote, cert, func() error {
				return nil
			}); err != nil {
				errList = append(errList, err)
				continue
			}

			isCertReady := false
			for _, cond := range cert.Status.Conditions {
				if cond.Type == certv1.CertificateConditionReady && string(cond.Status) == string(metav1.ConditionTrue) {
					isCertReady = true
					continue
				}
			}
			if isCertReady {
				continue
			}
			errList = append(errList, fmt.Errorf("certificate %s is not ready", cert.Name))
		}
	}

	if len(errList) == 0 {
		return deploy.Done()
	}

	return deploy.Error(errors.NewAggregate(errList))
}

func (t *TiDBGroupCert) genOrDeleteTiDBServerCerts(ctx context.Context, remote client.Client, group cloudv1.ExtraGroupSpec, cn *namer.Namer) ([]*v1.Certificate, error) {
	logger := log.FromContext(ctx).WithValues("group", group.Name)

	tidbServerCerts := t.tidbServerCerts(cn)
	if !t.Cluster.IsDNSDisabled(group.Name) {
		return tidbServerCerts, nil
	}

	// If DNS is disabled, we need to delete the TiDB server certs if they exist.
	for _, c := range tidbServerCerts {
		logger.Info("DNS is disabled, try to delete TiDB server cert", "cert", c.Name)
		if err := k8s.DeleteObjectCompleted(ctx, remote, c); err != nil {
			return nil, err
		}
	}
	return nil, nil
}

// Certs are only stored in k8s, so it's safe to delete them by k8s GC
// Owner references of certs are set to TC, and certs will be auto GCed
// after TC is deleted
func (t *TiDBGroupCert) Delete(ctx context.Context) deploy.Result {
	return deploy.Done()
}
