package action

import (
	"context"
	"fmt"

	v1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/pkg/cert"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	"github.com/tidbcloud/cluster-service-ng/pkg/k8s"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"
)

type CreateClusterCertAction struct {
	dataplaneManager dataplane.Manager
	sharedPoolRepo   repo.SharedPoolRepo
	tidbClusterRepo  repo.TiDBClusterRepo
	tidbGroupRepo    repo.TiDBGroupRepo
	dataplaneClient  dplanemgrv1.DataPlaneServiceClient
	bizConfig        *config.BizConfig
}

const CreateClusterCertActionName = "create-cluster-cert"

func NewCreateClusterCertAction(
	sharedPoolRepo repo.SharedPoolRepo,
	tidbClusterRepo repo.TiDBClusterRepo,
	tidbGroupRepo repo.TiDBGroupRepo,
	dataplaneManager dataplane.Manager,
	dataplaneClient dplanemgrv1.DataPlaneServiceClient,
	bizConfig *config.BizConfig,
) actions.Action {
	return &CreateClusterCertAction{
		sharedPoolRepo:   sharedPoolRepo,
		tidbClusterRepo:  tidbClusterRepo,
		tidbGroupRepo:    tidbGroupRepo,
		dataplaneManager: dataplaneManager,
		dataplaneClient:  dataplaneClient,
		bizConfig:        bizConfig,
	}
}

func (a *CreateClusterCertAction) Name() string {
	return CreateClusterCertActionName
}

func (a *CreateClusterCertAction) Invoke(args actions.ActionArgs) error {
	// generate context
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "create cluster cert action invoked")

	sharedPoolId, err := args.GetString(workflow.SharedPoolIdKey)
	if err != nil {
		return fmt.Errorf("failed to get sharedPool id: %w", err)
	}

	// load sharedPool
	sharedPool, err := a.sharedPoolRepo.Load(ctx, sharedPoolId, false)
	if err != nil {
		tilog.Error(ctx, "failed to load sharedPool", zap.String("sharedPoolId", sharedPoolId), zap.Error(err))
		return fmt.Errorf("failed to load sharedPool: %w", err)
	}

	sharedPoolStr, err := args.GetString(workflow.SharedPoolKey)
	if err != nil {
		return fmt.Errorf("failed to get sharedPool: %w", err)
	}

	var tempSharedPool entity.SharedPool
	err = protojson.Unmarshal([]byte(sharedPoolStr), &tempSharedPool)
	if err != nil {
		tilog.Error(ctx, "failed to unmarshal sharedPool", zap.Error(err))
		return fmt.Errorf("failed to unmarshal sharedPool: %w", err)
	}

	sharedPool.ComponentGroups = tempSharedPool.GetComponentGroups()

	if len(sharedPool.GetComponentGroups()) == 0 {
		tilog.Error(ctx, "sharedPool has no component groups", zap.String("sharedPoolId", sharedPoolId))
		return fmt.Errorf("sharedPool has no component groups: %s", sharedPoolId)
	}

	certOpts := CertOptionsFromBizConfig(a.bizConfig)
	certs := ComponentCerts(ctx, sharedPool, args, certOpts...)

	remoteCli, err := a.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		tilog.Error(ctx, "failed to get remote client for shared pool", zap.String("dataplaneName", sharedPool.GetDataplaneName()), zap.Error(err))
		return fmt.Errorf("failed to get remote client for shared pool: %w", err)
	}

	if entity.HasSharedTiProxy(sharedPool) {
		for _, comp := range sharedPool.GetComponentGroups() {
			if comp.GetComponentType() != apiv1.ComponentType_TIPROXY {
				continue
			}
			gatewayIdentity := utils.FnvHash(sharedPool.GetId())
			domainSuffix, err := GetDomainSuffix(ctx, sharedPool, a.dataplaneClient)
			if err != nil {
				tilog.Error(ctx, "failed to get domain suffix", zap.Error(err))
				return fmt.Errorf("failed to get domain suffix: %w", err)
			}
			certs = append(certs,
				cert.TiProxyServerCerts(comp.GetId(), sharedPool.GetDataPlaneNamespace(), gatewayIdentity, domainSuffix, a.bizConfig.EnablePca, certOpts...),
			)
			break
		}
	} else if tidbClusterId, err := args.GetString(workflow.TiDBClusterIdKey); err == nil {
		// load tidb cluster
		tidbCluster, err := a.tidbClusterRepo.Load(ctx, tidbClusterId, false)
		if err != nil {
			tilog.Error(ctx, "failed to load tidb cluster", zap.String("tidbClusterId", tidbClusterId), zap.Error(err))
			return fmt.Errorf("failed to load tidb cluster: %w", err)
		}

		domainSuffix, err := GetDomainSuffix(ctx, sharedPool, a.dataplaneClient)
		if err != nil {
			tilog.Error(ctx, "failed to get domain suffix", zap.Error(err))
			return fmt.Errorf("failed to get domain suffix: %w", err)
		}

		certs = append(certs,
			cert.TiProxyServerCerts(tidbCluster.GetId(), sharedPool.GetDataPlaneNamespace(), tidbCluster.GetGatewayIdentity(), domainSuffix, a.bizConfig.EnablePca, certOpts...),
		)
	}

	for _, c := range certs {
		if err := k8s.CreateIfNotExist(ctx, remoteCli, c); err != nil {
			tilog.Error(ctx, "failed to CreateIfNotExist cert", zap.Any("cert", c), zap.Error(err))
			return err
		}
		tilog.Info(ctx, "cert CreateIfNotExist", zap.String("certName", c.Name), zap.String("namespace", c.Namespace))
	}

	return nil
}

func CertOptionsFromBizConfig(bizConfig *config.BizConfig) []cert.Option {
	if bizConfig == nil || !bizConfig.CertificateDuration.HasOverride() {
		return nil
	}
	return []cert.Option{
		cert.WithDurationConfig(cert.DurationConfig{
			TiDBServerDuration:    bizConfig.CertificateDuration.TiDBServer,
			TiProxyServerDuration: bizConfig.CertificateDuration.TiProxyServer,
			RenewBefore:           bizConfig.CertificateDuration.RenewBefore,
		}),
	}
}

// tidb-operator hardcode db-shared-pool-client and db-cluster-client certs, otherwise tc will be blocked to became ready
func ComponentCerts(ctx context.Context, sharedPool *entity.SharedPool, args actions.ActionArgs, certOpts ...cert.Option) []*v1.Certificate {
	certs := make([]*v1.Certificate, 0, len(sharedPool.GetComponentGroups()))

	for _, comp := range sharedPool.GetComponentGroups() {

		if comp.GetComponentType() == apiv1.ComponentType_TIDB {
			continue
		}

		// check if the component is target component
		if !workflow.CheckIsTargetComponent(ctx, comp, args) {
			tilog.Info(ctx, "skip creating cert for component", zap.String("componentId", comp.GetId()), zap.String("componentType", comp.GetComponentType().String()))
			continue
		}

		c := cert.ComponentCert(entity.GetComponentTypeName(comp), comp.GetId(), sharedPool.GetDataPlaneNamespace())
		certs = append(certs, c)
	}

	certs = append(certs,
		cert.ClusterClientCert(sharedPool.GetDataPlaneNamespace()),            // db-cluster-client-secret and
		cert.TiDBServerCerts(sharedPool.GetDataPlaneNamespace(), certOpts...), // global tidb server secret
		cert.TiDBComponentCert(sharedPool.GetDataPlaneNamespace()),            // global tidb component cert
		cert.TiProxyTiDBCert(sharedPool.GetDataPlaneNamespace()),              // global tidb proxy cert
	)

	return certs
}

func GetDomainSuffix(ctx context.Context, sharedPool *entity.SharedPool, dataplaneClient dplanemgrv1.DataPlaneServiceClient) (string, error) {
	d, err := dataplaneClient.GetDataPlane(ctx, &dplanemgrv1.GetDataPlaneRequest{
		DataPlaneId: sharedPool.GetDataplaneName(),
	})
	if err != nil {
		return "", err
	}

	return d.GetDomain(), nil
}
