package dataplane

import (
	"context"
	"encoding/base64"
	"fmt"
	"strings"

	"github.com/pingcap/log"
	bizerr "github.com/tidbcloud/infra-provider/regional-server/pkg/common/errors"
	rsconfig "github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/task/common"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/helm"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/emptypb"
	"gopkg.in/yaml.v2"
	"helm.sh/helm/v3/pkg/chart"
	corev1 "k8s.io/api/core/v1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	dmpbv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/dataplane-manager/v1"
)

const (
	// The configuration for Kong audit log is the same in all dataplanes in this region.
	// So we can use a fixed name for the ConfigMap in seed and dataplane clusters.
	// NOTE: this name should match the one used in helm chart template.
	kongAuditConfigMapNameInSeed      = "kong-audit-config"
	kongAuditConfigMapNameInDataplane = "kong-audit-config"
	kongAuditConfigKey                = "config.yaml"
)

// SetKongAuditLogSetting sets the Kong audit log settings for the dataplane.
// 1. Create or update the ConfigMap in the seed cluster.
// 2. Propagate the ConfigMap to all dataplane clusters in the region.
func (s *DataPlaneService) SetKongAuditLogSetting(
	ctx context.Context, req *dmpbv1.SetKongAuditLogSettingRequest) (*emptypb.Empty, error) {
	// Step 1: Create or update the ConfigMap in seed cluster (control plane)
	configDataInSeed, err := s.buildKongAuditLogConfigData(req, req.BucketUrl)
	if err != nil {
		return nil, bizerr.Wrap(bizerr.InternalServerError, "failed to build kong audit config data for seed", err)
	}

	cmInSeed := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: s.config.Namespace,
			Name:      kongAuditConfigMapNameInSeed,
		},
		Data: map[string]string{
			kongAuditConfigKey: configDataInSeed,
		},
	}
	_, err = controllerutil.CreateOrUpdate(ctx, s.localCli, cmInSeed, func() error {
		cmInSeed.Data = map[string]string{
			kongAuditConfigKey: configDataInSeed,
		}
		return nil
	})
	if err != nil {
		return nil, bizerr.Wrap(bizerr.K8SClientError, "failed to create or update kong audit configmap in seed cluster", err)
	}
	log.Info("kong audit configmap in seed cluster created or updated")

	// Step 2: Propagate the ConfigMap to all dataplane clusters in the region.
	dpNamespace, err := s.getKongDataplaneNamespace()
	if err != nil {
		return nil, bizerr.Wrap(bizerr.InternalServerError, "failed to get kong dataplane namespace", err)
	}
	dpResp, err := s.ListDataPlanes(ctx, &dmpbv1.ListDataPlanesRequest{})
	if err != nil {
		return nil, err
	}
	for _, dp := range dpResp.DataPlanes {
		// Append suffix to bucketURL for each dataplane
		bucketURL := appendBucketURLSuffixIfNeeded(req.BucketUrl, dp.Id)
		configDataInDP, err := s.buildKongAuditLogConfigData(req, bucketURL)
		if err != nil {
			return nil, bizerr.Wrap(
				bizerr.InternalServerError,
				fmt.Sprintf("failed to build kong audit config data for dataplane %s", dp.Id),
				err,
			)
		}

		cmInDP := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: dpNamespace,
				Name:      kongAuditConfigMapNameInDataplane,
			},
			Data: map[string]string{
				kongAuditConfigKey: configDataInDP,
			},
		}

		remoteCli, err := s.getDataPlaneK8sClient(dp.Kubernetes.Kubeconfig)
		if err != nil {
			return nil, bizerr.Wrap(
				bizerr.K8SClientError,
				fmt.Sprintf("failed to create k8s client for dataplane cluster %s", dp.Id),
				err,
			)
		}
		_, err = controllerutil.CreateOrUpdate(ctx, remoteCli, cmInDP, func() error {
			cmInDP.Data = map[string]string{
				kongAuditConfigKey: configDataInDP,
			}
			return nil
		})
		if err != nil {
			return nil, bizerr.Wrap(
				bizerr.K8SClientError,
				fmt.Sprintf("failed to create or update kong audit configmap in dataplane cluster %s", dp.Id),
				err,
			)
		}
		log.Info("kong audit configmap in dataplane cluster created or updated", zap.String("dataplaneID", dp.Id))

		// Step 3: Update service-proxy helm release to apply new audit config immediately
		if err := s.updateServiceProxyHelmRelease(ctx, dp.Kubernetes.Kubeconfig, bucketURL, dp.Id, req); err != nil {
			return nil, bizerr.Wrap(
				bizerr.InternalServerError,
				fmt.Sprintf("failed to update service-proxy helm release in dataplane cluster %s", dp.Id),
				err,
			)
		}
	}

	return &emptypb.Empty{}, nil
}

// buildKongAuditLogConfigData builds the config.yaml content for Kong audit log
func (s *DataPlaneService) buildKongAuditLogConfigData(req *dmpbv1.SetKongAuditLogSettingRequest, bucketURL string) (string, error) {
	if req == nil {
		return "", fmt.Errorf("request is required")
	}

	config := map[string]interface{}{
		"enableLog":               req.Enabled,
		"logDir":                  "/var/log/auditlog",
		"auditLogFileName":        "ops-audit.log",
		"maxSize":                 req.MaxSize,
		"maxLifetime":             req.MaxLifetime,
		"bucketURL":               bucketURL,
		"region":                  req.Region,
		"gracefulStopWaitSeconds": 20,
		"producerPort":            9080,
	}
	if req.ProviderIdentity != nil {
		switch s.config.Provider {
		case consts.ProviderTypeAWS, consts.ProviderTypeEKS:
			config["aws"] = map[string]interface{}{
				"roleARN":    req.ProviderIdentity.CloudRoleIdentity,
				"externalID": req.ProviderIdentity.ExternalId,
			}
		case consts.ProviderTypeAKS:
			sasToken := strings.TrimSpace(req.ProviderIdentity.CloudRoleIdentity)
			if sasToken != "" {
				config["azure"] = map[string]interface{}{
					"sasToken": sasToken,
				}
			}
		default:
			return "", fmt.Errorf("unsupported provider type for audit log: %s", s.config.Provider)
		}
	}

	data, err := yaml.Marshal(config)
	if err != nil {
		return "", fmt.Errorf("failed to marshal kong audit config: %w", err)
	}

	return string(data), nil
}

func (s *DataPlaneService) getKongDataplaneNamespace() (string, error) {
	version := s.config.APPBoxConfig.K8sVersion
	tplId := s.tmplConfig.GetDefaultDedicatedTemplateIdWithVersion(version)
	tpl, exist := s.tmplConfig.GetDedicatedTemplateWithVersion(tplId, version)
	if !exist {
		return "", fmt.Errorf("dedicated template %s not found for version %s", tplId, version)
	}
	return tpl.ClusterGateway.GetNamespace(), nil
}

func (s *DataPlaneService) getDataPlaneK8sClient(kubeconfig string) (client.Client, error) {
	decoded, err := base64.StdEncoding.DecodeString(kubeconfig)
	if err != nil {
		return nil, fmt.Errorf("failed to decode kubeconfig: %w", err)
	}

	cfg, err := clientcmd.RESTConfigFromKubeConfig(decoded)
	if err != nil {
		return nil, fmt.Errorf("failed to get rest config from kubeconfig: %w", err)
	}

	// create a k8s client with the default scheme,
	// as we only use it to manage core resources like ConfigMap now
	return client.New(cfg, client.Options{})
}

func appendBucketURLSuffixIfNeeded(bucketRUL, suffix string) string {
	if bucketRUL == "" || suffix == "" || strings.HasSuffix(bucketRUL, suffix) {
		return bucketRUL
	}
	if strings.HasSuffix(bucketRUL, "/") {
		return fmt.Sprintf("%s%s", bucketRUL, suffix)
	}
	return fmt.Sprintf("%s/%s", bucketRUL, suffix)
}

// updateServiceProxyHelmRelease updates the service-proxy helm release with new audit config
func (s *DataPlaneService) updateServiceProxyHelmRelease(
	ctx context.Context,
	kubeconfig string,
	bucketURL string,
	dataplaneID string,
	req *dmpbv1.SetKongAuditLogSettingRequest,
) error {
	// Get service proxy configuration from template
	version := s.config.APPBoxConfig.K8sVersion
	tplId := s.tmplConfig.GetDefaultDedicatedTemplateIdWithVersion(version)
	tpl, exist := s.tmplConfig.GetDedicatedTemplateWithVersion(tplId, version)
	if !exist {
		return fmt.Errorf("dedicated template %s not found for version %s", tplId, version)
	}

	// Get helm values and update audit configuration
	values := tpl.ServiceProxy.ToHelmValues()

	// Set config.auditLog to enable/disable audit logging in service-proxy
	if cfg, ok := values["config"].(map[string]interface{}); ok {
		cfg["auditLog"] = req.Enabled
	} else {
		values["config"] = map[string]interface{}{
			"auditLog": req.Enabled,
		}
	}

	// Set auditSidecar.config for the audit sidecar container
	// Uses the shared function to ensure path consistency with service_proxy.go
	var roleARN, externalID string
	if req.ProviderIdentity != nil {
		roleARN = strings.TrimSpace(req.ProviderIdentity.CloudRoleIdentity)
		externalID = strings.TrimSpace(req.ProviderIdentity.ExternalId)
	}
	if s.config.Provider == consts.ProviderTypeAKS {
		roleARN = ""
		externalID = ""
	}
	common.SetAuditSidecarConfig(values, &common.AuditSidecarInput{
		BucketURL:   bucketURL,
		Region:      req.Region,
		RoleARN:     roleARN,
		ExternalID:  externalID,
		SASToken:    azureSASTokenForAudit(req, s.config.Provider),
		MaxLifetime: int(ptr.Deref(req.MaxLifetime, 0)),
		MaxSize:     int(req.MaxSize),
	})

	// Create ClientGetter for the remote dataplane cluster
	getter, err := k8s.NewKubeconfigClientGetter(kubeconfig)
	if err != nil {
		return fmt.Errorf("failed to create kubeconfig client getter: %w", err)
	}

	// Apply the updated helm release
	pkg := &serviceProxyPackage{
		config: &tpl.ServiceProxy,
		values: values,
	}
	if err := helm.Apply(ctx, getter, pkg); err != nil {
		return fmt.Errorf("failed to apply service-proxy helm release: %w", err)
	}

	log.Info("service-proxy helm release updated successfully",
		zap.String("dataplaneID", dataplaneID),
		zap.String("region", strings.TrimSpace(req.Region)),
		zap.String("bucketURL", strings.TrimSpace(bucketURL)),
		zap.Int32("maxSize", req.MaxSize),
		zap.Int32("maxLifetime", ptr.Deref(req.MaxLifetime, 0)),
	)
	return nil
}

func azureSASTokenForAudit(req *dmpbv1.SetKongAuditLogSettingRequest, provider string) string {
	if provider != consts.ProviderTypeAKS || req == nil || req.ProviderIdentity == nil {
		return ""
	}
	return strings.TrimSpace(req.ProviderIdentity.GetCloudRoleIdentity())
}

// serviceProxyPackage implements helm.Package interface for service-proxy
type serviceProxyPackage struct {
	config *rsconfig.InfraComponentConfig
	values map[string]interface{}
}

var _ helm.Package = &serviceProxyPackage{}

func (p *serviceProxyPackage) GetName() string {
	return p.config.GetName()
}

func (p *serviceProxyPackage) GetNamespace() string {
	return p.config.GetNamespace()
}

func (p *serviceProxyPackage) GetCRDs() ([]*apiextensionsv1.CustomResourceDefinition, error) {
	return nil, nil
}

func (p *serviceProxyPackage) ToHelmChart() (*chart.Chart, error) {
	return p.config.ToHelmChart()
}

func (p *serviceProxyPackage) ToHelmValues() map[string]interface{} {
	return p.values
}
