package common

import (
	"context"
	"fmt"
	"time"

	v1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"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/util"
)

type Cert struct {
	*task.BaseTask
}

func NewCert(ctx *task.Context) *Cert {
	return &Cert{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

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

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

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

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

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

func (t *Cert) componentCerts() (certs []*v1.Certificate) {
	// automatically create/delete component cert for ticdc even if it's not enabled.
	components := []cloudv1.ComponentType{cloudv1.ComponentTiCDC}
	for _, comp := range t.Cluster.GetComponents() {
		components = append(components, comp.GetComponentType())
	}
	for _, comp := range components {
		c := t.Namer.WithComponent(string(comp))
		cert := certutil.ComponentCert(c, t.selfSignedClusterComponents())
		certs = append(certs, cert)
	}

	// Note:
	//  tidb-operator hardcode db-tidb-cluster-client and db-cluster-client certs, otherwise tc will be blocked to became ready
	// db-tidb-client-secret. It's for compatibility only and shouldn't be used.
	certs = append(certs, certutil.TiDBClientCert(t.Namer, t.shortLivedComponentCert()))
	// db-cluster-client-secret
	certs = append(certs, certutil.ClusterClientCert(t.Namer, t.selfSignedClusterComponents()))
	return
}

func (t *Cert) tidbServerCerts() (certs []*v1.Certificate) {
	certs = append(certs, certutil.TiDBServerCert(t.Namer.WithComponent(string(cloudv1.ComponentTiDB)), t.useWildcardDNS(), t.selfSignedTiDBServer(), t.shortLivedComponentCert()))
	return
}

func (t *Cert) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	cli := t.EnvProvider.GetClient()

	// Label the create resources so the resources won't be leaked with the controller
	// upgrade as long as the names of the labels stay the same.
	cli = k8s.NewMutClientWithOwner(cli, t.Cluster.DeepCopy(), t.Namer).Client()

	certs := []*v1.Certificate{}

	certs = append(certs, t.componentCerts()...)
	tidbServerCerts, err := t.genOrDeleteTiDBServerCerts(ctx, cli)
	if err != nil {
		return deploy.Error(err)
	}
	certs = append(certs, tidbServerCerts...)

	for _, c := range certs {
		// CreateOrUpdate create the cert if it doesn't exist, and doesn't update it if it does.
		// If you need to update the cert, you need to add logic in the mutateFn
		if _, err := controllerutil.CreateOrUpdate(ctx, cli, c, func() error {
			return nil
		}); err != nil {
			return deploy.ErrorOrDone(err)
		}
	}
	return deploy.Done()
}

func (t *Cert) genOrDeleteTiDBServerCerts(ctx context.Context, remote client.Client) ([]*v1.Certificate, error) {
	logger := log.FromContext(ctx)
	tidbServerCerts := t.tidbServerCerts()
	if !t.Cluster.IsDNSDisabled(t.Cluster.GetDefaultGroupID()) {
		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
}

func (t *Cert) PostCreate(ctx context.Context) deploy.Result {
	cli := t.EnvProvider.GetClient()
	certList := &v1.CertificateList{}
	optCert := k8s.ListOpts[*v1.Certificate]{
		ObjectList: certList,
		Unpack:     func() []*v1.Certificate { return k8s.MapPtr(certList.Items) },
	}
	validErr, err := optCert.AllValid(ctx, cli,
		func(cert *v1.Certificate) error {
			for _, cond := range cert.Status.Conditions {
				if cond.Type == v1.CertificateConditionReady && string(cond.Status) == string(metav1.ConditionTrue) {
					return nil
				}
			}
			return fmt.Errorf("certificate %s is not ready", cert.Name)
		},
		client.InNamespace(t.Namer.GetTiDBNamespace()),
	)
	if err != nil {
		return deploy.Error(err)
	}
	return deploy.ErrorOrDone(validErr)
}

func (t *Cert) Delete(ctx context.Context) deploy.Result {
	cli := t.EnvProvider.GetClient()

	// Remove the certs only if the TiDBCluster is removed already
	if !t.TCGetter.IsDeleted(ctx, cli, consts.ClusterPrefix, t.Namer.GetTiDBNamespace()) {
		return deploy.Error(fmt.Errorf("tc is not deleted"))
	}

	var certs []*v1.Certificate

	certs = append(certs, t.componentCerts()...)
	certs = append(certs, t.tidbServerCerts()...)

	for _, c := range certs {
		if err := k8s.DeleteObject(ctx, cli, c); err != nil {
			return deploy.Error(err)
		}
	}

	return deploy.Done()
}

func (t *Cert) PostDelete(ctx context.Context) deploy.Result {
	cli := t.EnvProvider.GetClient()

	var certs []*v1.Certificate

	certs = append(certs, t.componentCerts()...)
	certs = append(certs, t.tidbServerCerts()...)

	for _, c := range certs {
		if err := k8s.GetDeletedObject(ctx, cli, k8s.GetNamespacedName(c), &v1.Certificate{}); err != nil {
			return deploy.Error(err)
		}
	}

	return deploy.Done()
}
