package cluster

import (
	"errors"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/shared-provider/pkg/utils"
	"go.uber.org/multierr"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func (r *reconcileContext) reconcileCluster() error {
	var errs error
	if err := r.reconcilePublicEndpoint(); err != nil {
		errs = multierr.Append(errs, err)
	}
	if err := r.reconcilePrivateLink(); err != nil {
		errs = multierr.Append(errs, err)
	}
	if err := r.reconcileTidbCluster(); err != nil {
		errs = multierr.Append(errs, err)
	}
	if errs != nil {
		return errs
	}

	return r.observeCluster()
}

func (r *reconcileContext) reconcileTidbCluster() error {
	// for reliability, some operations are only taken when the cluster is running normally
	whenRunning := whenPhaseIn([]cloudv1.ClusterPhase{cloudv1.ClusterPhaseRunning})

	// apply tags
	if _, err := r.mutateTidbCluster(r.overlayTags); err != nil {
		return err
	}
	// reconcile tiflash
	if mutated, err := r.mutateTidbCluster(r.overlayTiFlash, whenRunning); err != nil {
		return err
	} else if mutated {
		if err := r.updateProgress(cloudv1.ClusterPhaseScaling, "tiflash is toggled"); err != nil {
			return err
		}
	}
	// scale tidbcluster
	if mutated, err := r.mutateTidbCluster(r.overlayReplicas, whenRunning); err != nil {
		return err
	} else if mutated {
		if err := r.updateProgress(cloudv1.ClusterPhaseScaling, "cluster scaling requested"); err != nil {
			return err
		}
	}

	return nil
}

func (r *reconcileContext) getPublicService() (*corev1.Service, error) {
	svc := &corev1.Service{}
	if err := r.Cache.Get(r.ctx,
		client.ObjectKey{
			Name:      getPublicServiceName(r.tcKey.Name),
			Namespace: r.tcKey.Namespace,
		}, svc); err != nil {
		if apierrors.IsNotFound(err) {
			return nil, nil
		}
		return nil, err
	}

	return svc, nil
}

func (r *reconcileContext) reconcilePublicEndpoint() error {
	if !r.cluster.Spec.SqlEndpoints.Public.IsEnabled() {
		if err := r.deletePublicEndpoint(false); err != nil {
			return err
		}

		return r.ensureStatus(func() error {
			r.cluster.Status.SqlEndpoints.PublicEndpoint = nil
			return nil
		})
	}

	observed, err := r.getPublicService()
	if err != nil {
		return err
	}

	// 1. create service
	if observed == nil {
		observed = r.newService(true)
		if err := r.createOrGetWithOwnerLabel(observed); err != nil {
			return err
		}
	}

	if err := r.reconcileTidbAccess(); err != nil {
		return err
	}

	// 2. wait service loadbalancer reconciled
	if len(observed.Status.LoadBalancer.Ingress) == 0 {
		return &utils.RequeueAfterError{RequeueAfter: defaultWaitInterval, Cause: errors.New("wait public tidb service")}
	}
	ingress := observed.Status.LoadBalancer.Ingress[0]

	// 3. add a dns alias record to the loadbalancer to avoid clients access the real address directly (which is
	// out of our control)
	dnsName := r.endpointDNS(EndpointTypePublic)
	if err := r.ensureDNSRecord(dnsAlias{
		name:         r.publicDnsRecordName(),
		hostedZoneId: r.PublicHostedZoneId,
		alias:        dnsName,
		address:      ingress.Hostname,
	}); err != nil {
		return err
	}

	// 4. checkpoint the alias dns name to cluster status
	return r.ensureStatus(func() error {
		r.cluster.Status.SqlEndpoints.PublicEndpoint = &cloudv1.SqlEndpointStatus{
			Host: dnsName,
			Port: defaultTiDBPort,
		}
		return nil
	})
}
