package sharedpool

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

	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/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/cloudtags"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	"github.com/tidbcloud/cluster-service-ng/pkg/tioperator"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func (r *sharedPoolRepoImpl) cvtSharedPoolToModel(sharedpool *entity.SharedPool) (*schema.SharedPool, []*schema.SharedPoolObjectStore, error) {
	if sharedpool == nil {
		return nil, nil, fmt.Errorf("sharedpool cannot be nil")
	}

	capacityMode, err := normalizeSharedPoolCapacityMode(sharedpool.GetCapacityMode())
	if err != nil {
		return nil, nil, err
	}
	pdDeploymentMode := entity.GetEffectivePDDeploymentMode(sharedpool.GetPdDeploymentMode()).String()
	enabledTiKVRemoteWorkers := ""
	if capacityMode == apiv1.CapacityMode_RCU_DRIVEN {
		enabledTiKVRemoteWorkers, err = entity.EnabledTiKVRemoteWorkersToString(sharedpool.GetEnabledTikvRemoteWorkers())
		if err != nil {
			return nil, nil, fmt.Errorf("invalid enabled TiKV remote workers: %w", err)
		}
	}

	sharedpoolModel := &schema.SharedPool{
		ID:                        sharedpool.GetId(),
		DataplaneName:             sharedpool.GetDataplaneName(),
		VcpusLimit:                sharedpool.GetVcpusLimit(),
		PlacementGroupEnabled:     sharedpool.GetProvider().GetPlacementGroupEnabled(),
		DeployType:                sharedpool.DeployType,
		TiflashMode:               sharedpool.TiflashMode,
		TemplateVersion:           sharedpool.TemplateVersion,
		CapacityMode:              capacityMode.String(),
		PDDeploymentMode:          pdDeploymentMode,
		EnabledTiKVRemoteWorkers:  enabledTiKVRemoteWorkers,
		ConfigCenterMetaProfileID: sharedpool.ConfigCenterMetaProfileId,
		HighAvailabilityType:      sharedpool.GetHighAvailabilityType().String(),
		EnableCapacityPlanner:     sharedpool.EnableCapacityPlanner,
	}
	cloudResourceTags, err := marshalStringMap(sharedpool.GetCloudResourceTags())
	if err != nil {
		return nil, nil, fmt.Errorf("failed to marshal cloud resource tags: %w", err)
	}
	sharedpoolModel.CloudResourceTags = cloudResourceTags

	var objectStores []*schema.SharedPoolObjectStore

	if sharedpool.GetObjectStorage() == nil {
		return sharedpoolModel, objectStores, fmt.Errorf("object storage is nil for shared pool %s", sharedpool.GetId())
	}

	objectStores = append(objectStores, &schema.SharedPoolObjectStore{
		SharedPoolID: sharedpool.GetId(),
		StoreType:    schema.SharedPoolObjectStoreTypeData,
		StoreRegion:  sharedpool.GetObjectStorage().GetRegion(),
		StoreBucket:  sharedpool.GetObjectStorage().GetBucket(),
		StorePrefix:  sharedpool.GetObjectStorage().GetPrefix(),
	})

	if sharedpool.GetInternalStorage() != nil {
		objectStores = append(objectStores, &schema.SharedPoolObjectStore{
			SharedPoolID: sharedpool.GetId(),
			StoreType:    schema.SharedPoolObjectStoreTypeInternal,
			StoreRegion:  sharedpool.GetInternalStorage().GetRegion(),
			StoreBucket:  sharedpool.GetInternalStorage().GetBucket(),
			StorePrefix:  sharedpool.GetInternalStorage().GetPrefix(),
		})
	}

	return sharedpoolModel, objectStores, nil
}

func (r *sharedPoolRepoImpl) cvtSharedPoolToEn(ctx context.Context, sharedpoolModel *schema.SharedPool) (*entity.SharedPool, error) {
	if sharedpoolModel == nil {
		return nil, fmt.Errorf("sharedpool model cannot be nil")
	}

	capacityMode, err := parseSharedPoolCapacityMode(sharedpoolModel.CapacityMode)
	if err != nil {
		return nil, err
	}
	enabledTiKVRemoteWorkers := entity.ParseEnabledTiKVRemoteWorkers(
		sharedpoolModel.EnabledTiKVRemoteWorkers,
		capacityMode,
	)
	cloudResourceTags, err := unmarshalStringMap(sharedpoolModel.CloudResourceTags)
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal cloud resource tags: %w", err)
	}
	cloudResourceTags = cloudtags.DecodeFromDataPlane(cloudResourceTags)

	if sharedpoolModel.DeletedAt.Valid {
		sp := &entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id:                       sharedpoolModel.ID,
				Phase:                    apiv1.SharedPool_UNAVAILABLE,
				DataplaneName:            sharedpoolModel.DataplaneName,
				VcpusLimit:               sharedpoolModel.VcpusLimit,
				CapacityMode:             capacityMode,
				EnabledTikvRemoteWorkers: enabledTiKVRemoteWorkers,
				PdDeploymentMode: entity.GetEffectivePDDeploymentMode(
					apiv1.PDDeploymentMode(apiv1.PDDeploymentMode_value[sharedpoolModel.PDDeploymentMode]),
				),
				CloudResourceTags:    cloudResourceTags,
				HighAvailabilityType: parseSharedPoolHAType(sharedpoolModel.HighAvailabilityType),
			},
			DeployType:                sharedpoolModel.DeployType,
			TiflashMode:               sharedpoolModel.TiflashMode,
			ConfigCenterMetaProfileId: sharedpoolModel.ConfigCenterMetaProfileID,
			EnableCapacityPlanner:     sharedpoolModel.EnableCapacityPlanner,
			CreatedAt:                 sharedpoolModel.CreatedAt,
			DeletedAt:                 &sharedpoolModel.DeletedAt.Time,
		}
		for _, store := range sharedpoolModel.ObjectStores {
			ob := &entity.ObjectStorage{
				Prefix: store.StorePrefix,
				Region: store.StoreRegion,
				Bucket: store.StoreBucket,
			}
			switch store.StoreType {
			case schema.SharedPoolObjectStoreTypeData:
				sp.ObjectStorage = ob
			case schema.SharedPoolObjectStoreTypeInternal:
				sp.InternalStorage = ob
			}
		}
		if err := applyPauseResumeFields(sp, sharedpoolModel); err != nil {
			return nil, err
		}
		return sp, nil
	}

	provider, err := r.dataplaneRepo.LoadByName(ctx, sharedpoolModel.DataplaneName)
	if err != nil {
		if status.Code(err) == codes.NotFound {
			// Dataplane Manager returns partial Provider metadata while a
			// Dataplane is still creating. Preserve it because immutable tags
			// carry the tenant identity used by later workflows.
			if provider == nil {
				provider = &entity.DataPlaneProvider{}
			}
			provider.PlacementGroupEnabled = sharedpoolModel.PlacementGroupEnabled
			// if the provider is not found, we can still return a SharedPool entity
			tilog.Warn(ctx, "dataplane provider not found, maybe not ready",
				zap.String("dataplaneName", sharedpoolModel.DataplaneName),
				zap.Error(err))
			sp := &entity.SharedPool{
				SharedPool: apiv1.SharedPool{
					Id:                       sharedpoolModel.ID,
					Phase:                    apiv1.SharedPool_CREATING,
					DataplaneName:            sharedpoolModel.DataplaneName,
					Provider:                 provider,
					VcpusLimit:               sharedpoolModel.VcpusLimit,
					CapacityMode:             capacityMode,
					EnabledTikvRemoteWorkers: enabledTiKVRemoteWorkers,
					PdDeploymentMode: entity.GetEffectivePDDeploymentMode(
						apiv1.PDDeploymentMode(apiv1.PDDeploymentMode_value[sharedpoolModel.PDDeploymentMode]),
					),
					CloudResourceTags:    cloudResourceTags,
					HighAvailabilityType: parseSharedPoolHAType(sharedpoolModel.HighAvailabilityType),
				},
				DeployType:                sharedpoolModel.DeployType,
				TiflashMode:               sharedpoolModel.TiflashMode,
				ConfigCenterMetaProfileId: sharedpoolModel.ConfigCenterMetaProfileID,
				EnableCapacityPlanner:     sharedpoolModel.EnableCapacityPlanner,
				CreatedAt:                 sharedpoolModel.CreatedAt,
			}
			if err := applyPauseResumeFields(sp, sharedpoolModel); err != nil {
				return nil, err
			}
			return sp, nil
		}
		return nil, fmt.Errorf("failed to load provider by name %s: %w", sharedpoolModel.DataplaneName, err)
	}
	provider.PlacementGroupEnabled = sharedpoolModel.PlacementGroupEnabled

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

	sharedpool, err := tioperator.GetCluster(ctx, remoteCli, sharedpoolModel.ID)
	if err != nil {
		if status.Code(err) == codes.NotFound {
			// if the shared pool is not found, we can still return a SharedPool entity
			sp := &entity.SharedPool{
				SharedPool: apiv1.SharedPool{
					Id:                       sharedpoolModel.ID,
					Phase:                    apiv1.SharedPool_CREATING,
					DataplaneName:            sharedpoolModel.DataplaneName,
					Provider:                 provider,
					VcpusLimit:               sharedpoolModel.VcpusLimit,
					CapacityMode:             capacityMode,
					EnabledTikvRemoteWorkers: enabledTiKVRemoteWorkers,
					PdDeploymentMode: entity.GetEffectivePDDeploymentMode(
						apiv1.PDDeploymentMode(apiv1.PDDeploymentMode_value[sharedpoolModel.PDDeploymentMode]),
					),
					CloudResourceTags:    cloudResourceTags,
					HighAvailabilityType: parseSharedPoolHAType(sharedpoolModel.HighAvailabilityType),
				},
				DeployType:                sharedpoolModel.DeployType,
				TiflashMode:               sharedpoolModel.TiflashMode,
				ConfigCenterMetaProfileId: sharedpoolModel.ConfigCenterMetaProfileID,
				EnableCapacityPlanner:     sharedpoolModel.EnableCapacityPlanner,
			}
			if err := applyPauseResumeFields(sp, sharedpoolModel); err != nil {
				return nil, err
			}
			return sp, nil
		}

		return nil, fmt.Errorf("failed to get shared pool from tioperator: %w", err)
	}
	sharedpool.DeployType = sharedpoolModel.DeployType
	sharedpool.TiflashMode = sharedpoolModel.TiflashMode
	sharedpool.EnableCapacityPlanner = sharedpoolModel.EnableCapacityPlanner
	if err := applyPauseResumeFields(sharedpool, sharedpoolModel); err != nil {
		return nil, err
	}

	// if backup component is not ready, the shared pool is not ready
	backupGcSetting, err := r.backupGcSettingRepo.Load(ctx, sharedpoolModel.ID)
	if err != nil {
		if status.Code(err) == codes.NotFound {
			sharedpool.Phase = apiv1.SharedPool_CREATING
		} else {
			return nil, fmt.Errorf("failed to load backup gc setting: %w", err)
		}
	} else {
		if backupGcSetting.Status == schema.BackupGcStatusSPCreating {
			sharedpool.Phase = apiv1.SharedPool_CREATING
		}
	}

	sharedpool.Provider = provider
	sharedpool.DataplaneName = sharedpoolModel.DataplaneName
	sharedpool.VcpusLimit = sharedpoolModel.VcpusLimit
	sharedpool.TemplateVersion = sharedpoolModel.TemplateVersion
	sharedpool.ConfigCenterMetaProfileId = sharedpoolModel.ConfigCenterMetaProfileID
	sharedpool.CreatedAt = sharedpoolModel.CreatedAt
	sharedpool.CloudResourceTags = cloudResourceTags

	for _, store := range sharedpoolModel.ObjectStores {
		ob := &entity.ObjectStorage{
			Prefix: store.StorePrefix,
		}
		// get bucket name from dataplane manager
		bucket, err := r.bucketClient.GetBucket(ctx, &dplanemgrv1.GetBucketRequest{
			DataplaneId: sharedpool.GetDataplaneName(),
			BucketId:    store.StoreBucket,
		})

		switch {
		case err != nil:
			tilog.Warn(ctx, "failed to get data bucket from dataplane manager", zap.String("bucketId", store.StoreBucket), zap.Error(err))
		case bucket.GetName() == "":
			tilog.Warn(ctx, "bucket name is empty, maybe not ready", zap.String("bucketId", store.StoreBucket))
		default:
			ob.Bucket = bucket.GetName()
			ob.Region = bucket.GetRegion()
		}

		switch store.StoreType {
		case schema.SharedPoolObjectStoreTypeData:
			sharedpool.ObjectStorage = ob
		case schema.SharedPoolObjectStoreTypeInternal:
			sharedpool.InternalStorage = ob
		default:
			tilog.Warn(ctx, "unknown object store type", zap.String("storeType", store.StoreType))
		}
	}

	sharedpool.GatewayBaseUrl = provider.GetGatewayUrl()
	if !strings.HasPrefix(sharedpool.GatewayBaseUrl, "http") {
		sharedpool.GatewayBaseUrl = fmt.Sprintf("http://%s", sharedpool.GatewayBaseUrl)
	}

	sharedpool.CapacityMode = capacityMode
	sharedpool.PdDeploymentMode = entity.GetEffectivePDDeploymentMode(
		apiv1.PDDeploymentMode(apiv1.PDDeploymentMode_value[sharedpoolModel.PDDeploymentMode]),
	)
	sharedpool.EnabledTikvRemoteWorkers = enabledTiKVRemoteWorkers
	sharedpool.HighAvailabilityType = parseSharedPoolHAType(sharedpoolModel.HighAvailabilityType)

	return sharedpool, nil
}

func applyPauseResumeFields(sharedPool *entity.SharedPool, model *schema.SharedPool) error {
	state := entity.PauseResumeStateNone
	if model.PauseResumeState != nil {
		state = entity.PauseResumeState(*model.PauseResumeState)
	}
	switch state {
	case entity.PauseResumeStateNone, entity.PauseResumeStatePausing, entity.PauseResumeStatePaused, entity.PauseResumeStateResuming:
	default:
		return fmt.Errorf("unknown pause/resume state %q", state)
	}
	sharedPool.PauseResumeState = state
	sharedPool.SuspendComputeDesired = model.SuspendComputeDesired
	return nil
}

func marshalStringMap(tags map[string]string) (string, error) {
	if tags == nil {
		return "{}", nil
	}
	data, err := json.Marshal(tags)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

func unmarshalStringMap(raw string) (map[string]string, error) {
	if strings.TrimSpace(raw) == "" {
		return map[string]string{}, nil
	}
	var tags map[string]string
	if err := json.Unmarshal([]byte(raw), &tags); err != nil {
		return nil, err
	}
	if tags == nil {
		return map[string]string{}, nil
	}
	return tags, nil
}

// parseSharedPoolHAType converts the persisted HA type string into the canonical
// output enum. Empty or unknown values resolve to HIGH_AVAILABILITY_TYPE_UNSPECIFIED
// — the only valid canonical state for a row whose HA preference was never recorded.
// Callers that need a non-empty default must resolve it themselves (the Create API
// boundary does this via serverImpl.resolveCreateSharedPoolHAType).
func parseSharedPoolHAType(value string) apiv1.SharedPool_HighAvailabilityType {
	name := strings.TrimSpace(value)
	if name == "" {
		return apiv1.SharedPool_HIGH_AVAILABILITY_TYPE_UNSPECIFIED
	}
	if v, ok := apiv1.SharedPool_HighAvailabilityType_value[name]; ok {
		return apiv1.SharedPool_HighAvailabilityType(v)
	}
	return apiv1.SharedPool_HIGH_AVAILABILITY_TYPE_UNSPECIFIED
}

func parseSharedPoolCapacityMode(value string) (apiv1.CapacityMode, error) {
	name := strings.TrimSpace(value)
	if name == "" {
		return normalizeSharedPoolCapacityMode(apiv1.CapacityMode_CAPACITY_MODE_UNSPECIFIED)
	}

	mode, ok := apiv1.CapacityMode_value[name]
	if !ok {
		return apiv1.CapacityMode_CAPACITY_MODE_UNSPECIFIED, fmt.Errorf("invalid shared pool capacity mode %q", value)
	}

	return normalizeSharedPoolCapacityMode(apiv1.CapacityMode(mode))
}

func normalizeSharedPoolCapacityMode(mode apiv1.CapacityMode) (apiv1.CapacityMode, error) {
	switch mode {
	case apiv1.CapacityMode_CAPACITY_MODE_UNSPECIFIED,
		apiv1.CapacityMode_RCU_DRIVEN,
		apiv1.CapacityMode_TOPOLOGY_DRIVEN:
		return entity.GetEffectiveCapacityMode(mode), nil
	default:
		return apiv1.CapacityMode_CAPACITY_MODE_UNSPECIFIED, fmt.Errorf("invalid shared pool capacity mode %s", mode.String())
	}
}
