package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/textproto"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/aliyun/alibaba-cloud-sdk-go/services/ecs"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
	osscredentials "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/bufbuild/protovalidate-go"
	"github.com/gin-gonic/gin"
	grpclogging "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
	protovalidate_middleware "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidate"
	grpcrecovery "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
	gwruntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"github.com/pingcap/log"
	opv2br "github.com/pingcap/tidb-operator/api/v2/br/v1alpha1"
	opv2core "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/cluster-agent/v1"
	"github.com/tidbcloud/service-proxy/pkg/audit"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	tigrpcc "github.com/tidbcloud/tiinf/transport/tigrpc/client"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"google.golang.org/api/compute/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/health"
	"google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/cluster-service-ng/internal/cluster-agent/crypto"
	"github.com/tidbcloud/cluster-service-ng/internal/cluster-agent/internal/objectstore"
	"github.com/tidbcloud/cluster-service-ng/internal/cluster-agent/metrics"
	"github.com/tidbcloud/cluster-service-ng/internal/cluster-agent/service"
)

const (
	baseAPIPath = "/api/v1"
	// baseAPIPathWithPrefix when cluster-agent is deployed behind Cluster Gateway, a prefix like '/cluster-agent/tidb123'
	// is added.
	baseAPIPathWithPrefix = "/cluster-agent/:tcns" + baseAPIPath
	healthzEndpoint       = "/healthz"
	grpcHealthServiceName = "grpc.health.v1.Health"
	grpcHealthMethodName  = "Check"
)

func setupAndRunGRPCServer(ctx context.Context, cfg *Config, agentSvc *service.AgentService) {
	addr := cfg.InternalGRPCAddr
	validator, err := protovalidate.New()
	if err != nil {
		tilog.Fatal(ctx, "Failed to create proto validator", zap.Error(err))
	}

	lis, err := net.Listen("tcp", addr)
	if err != nil {
		tilog.Fatal(ctx, "Failed to listen for gRPC server", zap.String("addr", addr), zap.Error(err))
	}
	s := grpc.NewServer(
		grpc.StatsHandler(otelgrpc.NewServerHandler()),
		grpc.ChainUnaryInterceptor(
			grpcrecovery.UnaryServerInterceptor(grpcrecovery.WithRecoveryHandlerContext(recoveryHandler)),
			grpclogging.UnaryServerInterceptor(InterceptorLogger(log.L(), cfg.LogHealthCheck)),
			protovalidate_middleware.UnaryServerInterceptor(validator),
		),
	)

	apiv1.RegisterClusterAgentServiceServer(s, agentSvc)
	// refer to https://grpc.io/docs/guides/health-checking/
	grpc_health_v1.RegisterHealthServer(s, health.NewServer())
	tilog.Info(ctx, "Starting internal gRPC server", zap.String("addr", addr))
	go func() {
		if err2 := s.Serve(lis); err2 != nil {
			tilog.Info(ctx, "Internal gRPC server returned with error", zap.String("addr", addr), zap.Error(err2))
		}
	}()

	<-ctx.Done()

	tilog.Info(ctx, "Stopping internal gRPC server...", zap.String("addr", addr))
	s.GracefulStop()
	tilog.Info(ctx, "Internal gRPC server stopped", zap.String("addr", addr))
}

// buildAgentService constructs the shared AgentService used by both the gRPC server
// and the HTTP gateway. It is called once in main before launching the two goroutines.
func buildAgentService(ctx context.Context, cfg *Config) *service.AgentService {
	provider := cfg.Provider

	k8sConfig := buildK8sConfig(ctx, cfg)
	dataplaneCli := buildDataplaneCliFromConfig(k8sConfig)

	metricsCli, err := metrics.NewClient(k8sConfig)
	if err != nil {
		tilog.Warn(ctx, "failed to create metrics client, metrics API will be unavailable", zap.Error(err))
		metricsCli = nil
	}

	encryptor, err := crypto.NewEncryptor()
	if err != nil {
		tilog.Fatal(ctx, "failed to create encryptor", zap.Error(err))
	}

	tikvHTTPClient, err := buildTiKVTLSClient()
	if err != nil {
		tilog.Warn(ctx, "failed to build TiKV TLS client, metrics proxy will be unavailable", zap.Error(err))
		tikvHTTPClient = nil
	}

	var opts []service.Option
	var nodeRestarter service.NodeRestartProvider
	switch provider {
	case "aws":
		awsCfg, err := awsconfig.LoadDefaultConfig(ctx)
		if err != nil {
			tilog.Fatal(ctx, "failed to load aws config: %v", zap.Error(err))
		}
		awsCfg.Region = cfg.Region
		awsClient := ec2.NewFromConfig(awsCfg)
		nodeRestarter = service.NewAWSNodeRestarter(awsClient)
		opts = append(opts, service.WithPlanReplayerStore(objectstore.NewS3Store(s3.NewFromConfig(awsCfg))))
	case "gcp":
		//nolint:contextcheck // NewClientSet creates its own context internally
		computeSvc, err := compute.NewService(ctx)
		if err != nil {
			tilog.Fatal(ctx, "failed to create gcp compute service: %v", zap.Error(err))
		}
		nodeRestarter = service.NewGCPNodeRestarter(computeSvc)
	case "kind":
		nodeRestarter = service.NewKindNodeRestarter()
	case "azure":
		cred, err := azidentity.NewDefaultAzureCredential(nil)
		if err != nil {
			tilog.Fatal(ctx, "failed to get azure default credential: %v", zap.Error(err))
		}
		nodeRestarter = service.NewAzureNodeRestarter(cred)
	case "alicloud":
		ecsClient, err := ecs.NewClientWithEcsRamRole(cfg.Region, "")
		if err != nil {
			tilog.Fatal(ctx, "failed to create alicloud ecs client: %v",
				zap.String("regionId", cfg.Region),
				zap.Error(err),
			)
		}
		nodeRestarter = service.NewAlicloudNodeRestarter(ecsClient)

		credentialsProvider := osscredentials.NewEcsRoleCredentialsProvider()
		// Presigning must use a public client; otherwise the generated URL embeds
		// an OSS internal hostname that callers outside the VPC cannot resolve.
		internalOSSConfig := oss.LoadDefaultConfig().
			WithCredentialsProvider(credentialsProvider).
			WithRegion(cfg.Region).
			WithUseInternalEndpoint(true)
		publicOSSConfig := oss.LoadDefaultConfig().
			WithCredentialsProvider(credentialsProvider).
			WithRegion(cfg.Region).
			WithUseInternalEndpoint(false)
		opts = append(opts, service.WithPlanReplayerStore(objectstore.NewOSSStore(
			oss.NewClient(internalOSSConfig),
			oss.NewClient(publicOSSConfig),
		)))
	default:
		tilog.Fatal(ctx, "unknown provider", zap.String("provider", provider))
	}
	opts = append(opts,
		service.WithProvider(provider),
		service.WithNodeRestarter(nodeRestarter),
		service.WithMetricsClient(metricsCli),
		service.WithTiKVHTTPClient(tikvHTTPClient),
		service.WithPrivilegedTiDBDeactivationWait(cfg.PrivilegedTiDBDeactivationWait),
	)
	return service.NewAgentService(dataplaneCli, encryptor, opts...)
}

// buildTiKVTLSClient builds a reusable mTLS HTTP client for calling TiKV's status port
// using the client certificates mounted at /client-ssl/.
// Returns an error if the cert files are missing or invalid; callers should treat this
// as a graceful degradation (warn + nil client) rather than a fatal startup failure.
func buildTiKVTLSClient() (*http.Client, error) {
	cert, err := tls.LoadX509KeyPair("/client-ssl/tls.crt", "/client-ssl/tls.key")
	if err != nil {
		return nil, fmt.Errorf("failed to load client cert: %w", err)
	}
	caCert, err := os.ReadFile("/client-ssl/ca.crt")
	if err != nil {
		return nil, fmt.Errorf("failed to read CA cert: %w", err)
	}
	pool := x509.NewCertPool()
	if ok := pool.AppendCertsFromPEM(caCert); !ok {
		return nil, fmt.Errorf("failed to parse CA cert from /client-ssl/ca.crt")
	}
	return &http.Client{
		Timeout: 30 * time.Second,
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				Certificates: []tls.Certificate{cert},
				RootCAs:      pool,
				MinVersion:   tls.VersionTLS12,
			},
		},
	}, nil
}

func setupAndRunGRPCGateway(
	ctx context.Context,
	cfg *Config,
	agentSvc *service.AgentService,
	auditManager *audit.Manager,
) {
	addr := cfg.Addr
	grpcAddr := cfg.InternalGRPCAddr
	conn, err := tigrpcc.NewClientConn(grpcAddr, tigrpcc.WithDefault())
	if err != nil {
		tilog.Fatal(ctx, "Failed to dial gRPC server", zap.String("addr", grpcAddr), zap.Error(err))
	}
	defer func() {
		go func() {
			<-ctx.Done()
			if cerr := conn.Close(); cerr != nil {
				tilog.Info(ctx, "Failed to close conn to %s: %v", zap.String("addr", grpcAddr), zap.Error(cerr))
			}
		}()
	}()

	mux := gwruntime.NewServeMux(
		gwruntime.WithForwardResponseOption(httpResponseModifier),
		// refer to https://grpc-ecosystem.github.io/grpc-gateway/docs/operations/health_check/
		gwruntime.WithHealthEndpointAt(grpc_health_v1.NewHealthClient(conn), healthzEndpoint),
	)
	if err := apiv1.RegisterClusterAgentServiceHandler(ctx, mux, conn); err != nil {
		tilog.Fatal(ctx, "Failed to register gRPC gateway for cluster agent", zap.Error(err))
	}

	apiRouter := newGatewayRouter(cfg, mux, agentSvc, auditManager)
	srv := newGatewayHTTPServer(addr, apiRouter)

	lis, err := net.Listen("tcp", addr)
	if err != nil {
		tilog.Fatal(ctx, "Failed to listen for gRPC gateway", zap.String("addr", addr), zap.Error(err))
	}
	tlsEnabled, tlsErr := validateTLSFiles(cfg.TLSCertFile, cfg.TLSKeyFile)
	if tlsErr != nil {
		tilog.Fatal(ctx, "Invalid TLS config for gRPC gateway",
			zap.String("certFile", cfg.TLSCertFile),
			zap.String("keyFile", cfg.TLSKeyFile),
			zap.Error(tlsErr))
	}

	if tlsEnabled {
		tilog.Info(ctx, "Starting gRPC gateway with TLS",
			zap.String("addr", addr),
			zap.String("certFile", cfg.TLSCertFile),
		)
	} else {
		tilog.Info(ctx, "Starting gRPC gateway without TLS", zap.String("addr", addr))
	}

	go func() {
		var serveErr error
		if tlsEnabled {
			// TODO(cluster-agent): Reload TLS cert/key without restart to avoid serving expired certs after secret rotation.
			serveErr = srv.ServeTLS(lis, cfg.TLSCertFile, cfg.TLSKeyFile)
		} else {
			serveErr = srv.Serve(lis)
		}
		if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
			tilog.Fatal(ctx, "The gRPC gateway returned with error",
				zap.String("addr", addr), zap.Error(serveErr))
		}
	}()

	<-ctx.Done()

	// The context is used to inform the server it has 5 seconds to finish
	// the request it is currently handling
	// shutdownCtx is intentionally created from Background for graceful shutdown
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	tilog.Info(shutdownCtx, "Stopping gRPC gatway...", zap.String("addr", addr))
	if err := srv.Shutdown(shutdownCtx); err != nil {
		tilog.Error(shutdownCtx, "gRPC gateway forced to shutdown", zap.String("addr", addr), zap.Error(err))
	} else {
		tilog.Info(shutdownCtx, "gRPC gateway stopped", zap.String("addr", addr))
	}
}

// newGatewayRouter creates the HTTP gateway router with the following middleware chain:
//
//	Recovery → Logger → HTTPAuditMiddleware → ServePathStartFromAny → handler
//
// Middleware execution order:
//  1. Recovery: catches panics and logs errors
//  2. Logger: logs HTTP requests (skips health checks when logHealthCheck=false)
//  3. HTTPAuditMiddleware: captures originalPath *before* ServePathStartFromAny rewrites it,
//     and records audit log in post-handler phase using the cached originalPath
//  4. ServePathStartFromAny: rewrites path by stripping prefix, restores after handler
//  5. SetResponseTraceID: adds trace ID to response headers
//
// This ordering ensures audit logs record the original unprefixed path
// (e.g. "/cluster-agent/tidb123/api/v1/clusters/...") rather than the rewritten path.
func newGatewayRouter(cfg *Config, mux http.Handler, agentSvc *service.AgentService, auditManager *audit.Manager) *gin.Engine {
	apiRouter := gin.New()
	apiRouter.Use(
		gin.CustomRecovery(func(c *gin.Context, err any) {
			tilog.Error(c.Request.Context(), "panic triggered", zap.String("panic", fmt.Sprintf("%v", err)))
		}),
		otelgin.Middleware(serviceName),
		// middlewares.CommonLogging,
		gin.LoggerWithConfig(gin.LoggerConfig{
			Skip: func(c *gin.Context) bool {
				return shouldSkipHTTPHealthCheckLog(cfg.LogHealthCheck, httpHealthCheckPath(c), c.Writer.Status())
			},
		}),
		HTTPAuditMiddleware(auditManager, cfg.LogHealthCheck),
		ServePathStartFromAny,
		// tigin.RecordMetrics,
		SetResponseTraceID,
	)

	registerComponentMetricsRoutes(apiRouter, agentSvc)
	apiRouter.Any(healthzEndpoint, gin.WrapH(mux))
	apiRouter.NoRoute(newGatewayFallbackHandler(mux))
	return apiRouter
}

func registerComponentMetricsRoutes(router gin.IRoutes, agentSvc *service.AgentService) {
	// Register plain Gin routes for raw metrics proxy (direct, no grpc-gateway).
	// These routes must remain explicit because gin cannot mix them with a
	// catch-all under the same "/api/v1" prefix.
	componentMetricsPath := "/clusters/:cluster_id/components/:component_type/:pod_name/metrics"
	componentNodeMetricsPath := "/clusters/:cluster_id/components/:component_type/:pod_name/node-metrics"
	router.GET(baseAPIPath+componentMetricsPath, agentSvc.HandleComponentPodMetrics)
	router.GET(baseAPIPathWithPrefix+componentMetricsPath, agentSvc.HandleComponentPodMetrics)
	router.GET(baseAPIPath+componentNodeMetricsPath, agentSvc.HandleComponentPodNodeMetrics)
	router.GET(baseAPIPathWithPrefix+componentNodeMetricsPath, agentSvc.HandleComponentPodNodeMetrics)
}

func newGatewayFallbackHandler(mux http.Handler) gin.HandlerFunc {
	return func(c *gin.Context) {
		rewrittenPath, ok := rewriteGatewayPath(c.Request.URL.Path)
		if !ok {
			c.AbortWithStatus(http.StatusNotFound)
			return
		}

		originalPath := c.Request.URL.Path
		originalRawPath := c.Request.URL.RawPath

		c.Request.URL.Path = rewrittenPath
		if originalRawPath != "" {
			rewrittenRawPath, ok := rewriteGatewayPath(originalRawPath)
			if !ok {
				c.AbortWithStatus(http.StatusNotFound)
				return
			}
			c.Request.URL.RawPath = rewrittenRawPath
		}

		defer func() {
			c.Request.URL.Path = originalPath
			c.Request.URL.RawPath = originalRawPath
		}()

		// Gin enters NoRoute handlers with status pre-set to 404. Reset it so
		// grpc-gateway success responses that only write a body do not inherit 404.
		if !c.Writer.Written() && c.Writer.Status() == http.StatusNotFound {
			c.Status(http.StatusOK)
		}

		mux.ServeHTTP(c.Writer, c.Request)
		c.Abort()
	}
}

func rewriteGatewayPath(requestPath string) (string, bool) {
	if requestPath == healthzEndpoint {
		return requestPath, true
	}

	if strings.HasPrefix(requestPath, baseAPIPath+"/") {
		return strings.TrimPrefix(requestPath, baseAPIPath), true
	}

	const clusterAgentPrefix = "/cluster-agent/"
	if !strings.HasPrefix(requestPath, clusterAgentPrefix) {
		return "", false
	}

	remainingPath := strings.TrimPrefix(requestPath, clusterAgentPrefix)
	separatorIdx := strings.IndexByte(remainingPath, '/')
	if separatorIdx <= 0 {
		return "", false
	}

	rewrittenPath := remainingPath[separatorIdx:]
	if strings.HasPrefix(rewrittenPath, baseAPIPath+"/") {
		return strings.TrimPrefix(rewrittenPath, baseAPIPath), true
	}

	return "", false
}

func newGatewayHTTPServer(addr string, handler http.Handler) *http.Server {
	return &http.Server{
		Addr:              addr,
		Handler:           handler,
		ReadHeaderTimeout: 30 * time.Second,
		TLSConfig: &tls.Config{
			MinVersion: tls.VersionTLS12,
		},
	}
}

func validateTLSFiles(certFile, keyFile string) (bool, error) {
	if certFile == "" && keyFile == "" {
		return false, nil
	}
	if certFile == "" || keyFile == "" {
		return false, fmt.Errorf("both TLS cert file and key file must be set")
	}

	if err := validateReadableFile(certFile); err != nil {
		return false, fmt.Errorf("TLS cert file %q is not readable: %w", certFile, err)
	}
	if err := validateReadableFile(keyFile); err != nil {
		return false, fmt.Errorf("TLS key file %q is not readable: %w", keyFile, err)
	}

	if _, err := tls.LoadX509KeyPair(certFile, keyFile); err != nil {
		return false, fmt.Errorf("failed to load TLS cert/key pair (%q, %q): %w", certFile, keyFile, err)
	}
	return true, nil
}

func validateReadableFile(path string) error {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer func() {
		_ = f.Close()
	}()

	info, err := f.Stat()
	if err != nil {
		return err
	}
	if info.IsDir() {
		return fmt.Errorf("path is a directory")
	}
	return nil
}

func httpResponseModifier(ctx context.Context, w http.ResponseWriter, p proto.Message) error {
	md, ok := gwruntime.ServerMetadataFromContext(ctx)
	if !ok {
		return nil
	}

	// set http status code
	if vals := md.HeaderMD.Get(ResponseKeyStatusCode); len(vals) > 0 {
		code, err := strconv.Atoi(vals[0])
		if err != nil {
			return err
		}
		// delete the headers to not expose any grpc-metadata in http response
		delete(md.HeaderMD, ResponseKeyStatusCode)
		delete(w.Header(), fmt.Sprintf("Grpc-Metadata-%s", textproto.CanonicalMIMEHeaderKey(ResponseKeyStatusCode)))
		w.WriteHeader(code)
	}

	return nil
}

func recoveryHandler(ctx context.Context, p any) (err error) {
	tilog.Error(ctx, "panic triggered", zap.Any("panic", p))
	return status.Errorf(codes.Unknown, "panic triggered: %v", p)
}

// InterceptorLogger adapts zap logger to interceptor logger.
// This code is simple enough to be copied and not imported.
func InterceptorLogger(logger *zap.Logger, logHealthCheck bool) grpclogging.Logger {
	return grpclogging.LoggerFunc(func(ctx context.Context, lvl grpclogging.Level, msg string, fields ...any) {
		if shouldSkipGRPCHealthAccessLog(logHealthCheck, msg, fields...) {
			return
		}

		f := make([]zap.Field, 0, len(fields)/2)

		for i := 0; i+1 < len(fields); i += 2 {
			key := fields[i]
			value := fields[i+1]

			switch v := value.(type) {
			case string:
				f = append(f, zap.String(key.(string), v))
			case int:
				f = append(f, zap.Int(key.(string), v))
			case bool:
				f = append(f, zap.Bool(key.(string), v))
			default:
				f = append(f, zap.Any(key.(string), v))
			}
		}

		logger := logger.WithOptions(zap.AddCallerSkip(1)).With(f...).With(tilog.FieldsFromContext(ctx)...)

		levelMap := map[grpclogging.Level]zapcore.Level{
			grpclogging.LevelDebug: zap.DebugLevel,
			grpclogging.LevelInfo:  zap.InfoLevel,
			grpclogging.LevelWarn:  zap.WarnLevel,
			grpclogging.LevelError: zap.ErrorLevel,
		}
		zapLevel, ok := levelMap[lvl]
		if !ok {
			zapLevel = zap.InfoLevel
		}
		logger.Log(zapLevel, msg)
	})
}

func httpHealthCheckPath(c *gin.Context) string {
	if path := c.Param("any"); path != "" {
		return path
	}

	if path, ok := rewriteGatewayPath(c.Request.URL.Path); ok {
		return path
	}

	return c.Request.URL.Path
}

func shouldSkipHTTPHealthCheckLog(logHealthCheck bool, path string, statusCode int) bool {
	if logHealthCheck {
		return false
	}

	return path == healthzEndpoint && statusCode == http.StatusOK
}

func shouldSkipGRPCHealthAccessLog(logHealthCheck bool, msg string, fields ...any) bool {
	if logHealthCheck || !isGRPCHealthCheckByFields(fields...) {
		return false
	}

	switch msg {
	case "started call":
		return true
	case "finished call":
		code, ok := grpcLogFieldString(fields, "grpc.code")
		return ok && code == codes.OK.String()
	default:
		return false
	}
}

func isGRPCHealthCheckByFields(fields ...any) bool {
	service, ok := grpcLogFieldString(fields, "grpc.service")
	if !ok || service != grpcHealthServiceName {
		return false
	}

	method, ok := grpcLogFieldString(fields, "grpc.method")
	return ok && method == grpcHealthMethodName
}

// grpcLogFieldString extracts the string value for the given key from grpc logging fields.
// Fields are expected as key-value pairs: [k1, v1, k2, v2, ...].
// Returns the first match if duplicate keys exist.
func grpcLogFieldString(fields []any, key string) (string, bool) {
	for i := 0; i+1 < len(fields); i += 2 {
		fieldKey, ok := fields[i].(string)
		if !ok || fieldKey != key {
			continue
		}

		fieldVal, ok := fields[i+1].(string)
		if !ok {
			return "", false
		}
		return fieldVal, true
	}

	return "", false
}

// SetResponseTraceID is copied from tiinf/transport/tigin/middlewares.go to avoid import too many useless packages
func SetResponseTraceID(c *gin.Context) {
	spanCtx := trace.SpanContextFromContext(c.Request.Context())
	if spanCtx.HasTraceID() {
		c.Header("x-debug-trace-id", spanCtx.TraceID().String())
	}
	c.Next()
}

func buildK8sConfig(ctx context.Context, cfg *Config) *rest.Config {
	var config *rest.Config
	var err error
	if cfg.DataplaneKubeConfig != "" {
		tilog.Info(ctx, "Using dataplane kubeconfig file", zap.String("file", cfg.DataplaneKubeConfig))
		config, err = clientcmd.BuildConfigFromFlags("", cfg.DataplaneKubeConfig)
		if err != nil {
			tilog.Fatal(ctx, "failed to build dataplane config: %v", zap.Error(err))
		}
	} else {
		// create a k8s client with the in-cluster config
		tilog.Info(ctx, "Using in-cluster config")
		config, err = rest.InClusterConfig()
		if err != nil {
			tilog.Fatal(ctx, "failed to get in-cluster config: %v", zap.Error(err))
		}
	}
	return config
}

func buildDataplaneCliFromConfig(config *rest.Config) client.Client {
	scheme := runtime.NewScheme()
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
	utilruntime.Must(appsv1.AddToScheme(scheme))
	utilruntime.Must(opv2core.Install(scheme))
	utilruntime.Must(opv2br.Install(scheme))
	dataplaneCli, err := client.New(config, client.Options{Scheme: scheme})
	if err != nil {
		// Use logger if available, otherwise panic
		panic(fmt.Sprintf("failed to create dataplane client: %v", err))
	}
	return dataplaneCli
}
