package eventbus

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

	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"

	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	openapingv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/nextgen/v1beta2"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

type ConsoleAuditLogSender interface {
	SubmitAuditLog(
		ctx context.Context,
		eventType auditlog.EventType,
		reqMeta *metadata.RequestMeta,
		detailFunc func() auditlog.Details,
		rErr error,
	)
	SubmitAuditLogForNextgen(
		ctx context.Context,
		eventType auditlog.EventType,
		reqMeta *metadata.RequestMeta,
		detailFunc func() auditlog.Details,
		rErr error,
	)
}

const (
	TierTypeServerless string = "serverless"
	TierTypeDedicated  string = "dedicated"
	TierTypePremium    string = "premium"
)

// Detail-map keys specific to move operations.
const (
	KeySourceProjectID   = "source_project_id"
	KeyTargetProjectID   = "target_project_id"
	KeyTargetProjectName = "target_project_name"
	KeyServicePlans      = "service_plans"
	KeyTotal             = "total"
	KeySuccess           = "success"
	KeyFailed            = "failed"
	KeyMigrations        = "migrations"
)

// Detail-map keys for Dual-Layer Data Encryption (CMEK M2 PM spec).
const (
	KeyDualLayerDataEncryption = "dual-layer-data-encryption"
	KeyKmsKeyType              = "kms-key-type"
	KeyKmsKeyArn               = "kms-key-arn"

	KmsKeyTypeServiceManaged = "Service-managed Encryption Key"
	KmsKeyTypeCMEK           = "CMEK"
)

// TierTypeFromServicePlan returns the tier type string for a given service plan.
// This allows move-related audit log entries to record the correct tier type for
// any service plan, enabling future move support beyond Starter/Essential.
func TierTypeFromServicePlan(plan openapicommonv1beta1.ServicePlan) string {
	switch plan {
	case openapicommonv1beta1.ServicePlan_Dedicated:
		return TierTypeDedicated
	case openapicommonv1beta1.ServicePlan_Premium,
		openapicommonv1beta1.ServicePlan_Premium_Reserved,
		openapicommonv1beta1.ServicePlan_BYOC:
		return TierTypePremium
	default:
		return TierTypeServerless
	}
}

func NewAuditLogDetails(tierType string) auditlog.Details {
	return auditlog.Details{
		auditlog.KeyTierType: tierType,
	}
}

func AppendServerlessClusterDetails(
	details auditlog.Details,
	cluster *openapiserverlessv1beta1.Cluster,
) auditlog.Details {
	if cluster == nil {
		return details
	}

	if cluster.Region != nil {
		details[auditlog.KeyRegion] = cluster.Region.DisplayName
		m := common.RegionStrRegexp.FindStringSubmatch(cluster.Region.Name)
		if len(m) == 3 {
			details[auditlog.KeyCloudProvider] = m[1]
			if cluster.Region.DisplayName == "" {
				details[auditlog.KeyRegion] = m[2]
			}
		}
	}

	if cluster.SpendingLimit != nil {
		details[auditlog.KeySpendingLimit] = cluster.SpendingLimit.Monthly
	}
	if cluster.EncryptionConfig != nil {
		details[auditlog.KeyEnhancedEncryption] = cluster.EncryptionConfig.EnhancedEncryptionEnabled
	} else {
		details[auditlog.KeyEnhancedEncryption] = false
	}

	if cluster.AutomatedBackupPolicy != nil {
		details[auditlog.KeyBackupTime] = cluster.AutomatedBackupPolicy.StartTime
		details[auditlog.KeyBackupRetention] = cluster.AutomatedBackupPolicy.RetentionDays
		details[auditlog.KeyBackupType] = "auto"
	}
	if cluster.GetAutoScaling().GetMaxRcu() > 0 {
		details[auditlog.KeySpendingLimit] = fmt.Sprintf(
			"rcu: %v~%v",
			cluster.GetAutoScaling().GetMinRcu(),
			cluster.GetAutoScaling().GetMaxRcu(),
		)
	}

	return details
}

func AppendDedicatedClusterDetails(
	details auditlog.Details,
	cluster *openapidedicatedv1beta1.Cluster,
) auditlog.Details {
	if cluster == nil {
		return details
	}

	details[auditlog.KeyPortNumber] = cluster.Port
	// regionId: aws-us-west-1
	m := strings.Split(cluster.RegionId, "-")

	if len(m) == 4 {
		details[auditlog.KeyCloudProvider] = m[0]
		details[auditlog.KeyRegion] = strings.Join(m[1:], "-")
	}

	return details
}

func AppendNextgenClusterDetails(
	details auditlog.Details,
	cluster *openapingv1beta2.Tidb,
) auditlog.Details {
	if cluster == nil {
		return details
	}
	details[auditlog.KeyDisplayName] = cluster.DisplayName

	// regionId: aws-us-west-1
	m := strings.Split(cluster.RegionId, "-")

	if len(m) == 4 {
		details[auditlog.KeyCloudProvider] = m[0]
		details[auditlog.KeyRegion] = strings.Join(m[1:], "-")
	}

	details[auditlog.KeyServicePlan] = cluster.GetServicePlan()
	return details
}

func AppendDedicatedTidbGroupDetails(
	details auditlog.Details,
	tidbGroup *openapidedicatedv1beta1.TidbNodeGroup,
) auditlog.Details {
	if tidbGroup == nil {
		return details
	}

	details[auditlog.KeyTargetClusterId] = tidbGroup.GetClusterId()
	details[auditlog.KeyDisplayName] = tidbGroup.GetDisplayName()
	details[auditlog.KeyTidbNodeGroupId] = tidbGroup.GetTidbNodeGroupId()

	return details
}

// AppendDualLayerEncryptionDetails writes the three encryption audit fields
// (dual-layer-data-encryption / kms-key-type / kms-key-arn) required by the
// CMEK M2 PM spec. `enc` may be nil or NoEncryption — in both cases only the
// boolean field is set.
func AppendDualLayerEncryptionDetails(
	details auditlog.Details,
	enc *openapingv1beta2.Tidb_DualLayerDataEncryption,
) auditlog.Details {
	if details == nil {
		details = make(auditlog.Details)
	}
	switch {
	case enc == nil, enc.GetNoEncryption() != nil:
		details[KeyDualLayerDataEncryption] = false
	case enc.GetDefaultKey() != nil:
		details[KeyDualLayerDataEncryption] = true
		details[KeyKmsKeyType] = KmsKeyTypeServiceManaged
	case enc.GetCmek() != nil:
		details[KeyDualLayerDataEncryption] = true
		details[KeyKmsKeyType] = KmsKeyTypeCMEK
		if arn := enc.GetCmek().GetAwsKms().GetKmsKeyArn(); arn != "" {
			details[KeyKmsKeyArn] = arn
		}
	}
	return details
}

func AppendDbAuditLogDetails[T proto.Message](
	details auditlog.Details,
	clusterId uint64,
	clusterName string,
	content T,
) auditlog.Details {
	if details == nil {
		details = make(auditlog.Details)
	}
	details[auditlog.KeyTargetClusterId] = clusterId
	details[auditlog.KeyDisplayName] = clusterName
	// Check if content has any set fields.
	if proto.Size(content) > 0 {
		if m := structToMap(content); len(m) > 0 {
			details[auditlog.KeyGeneralContent] = m
		}
	}
	return details
}

func structToMap(obj proto.Message) map[string]any {
	if obj == nil {
		return nil
	}
	// Marshal to JSON using protojson which correctly handles protobuf specifics like enums.
	marshaler := protojson.MarshalOptions{
		UseProtoNames: true,
	}
	jsonBytes, err := marshaler.Marshal(obj)
	if err != nil {
		return nil
	}
	// Unmarshal the JSON into a map.
	var result map[string]any
	if err := json.Unmarshal(jsonBytes, &result); err != nil {
		return nil
	}
	return result
}
