package ruconfig

import (
	"context"
	"errors"
	"fmt"
	"strings"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"github.com/tidbcloud/pkgv2/db"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"gorm.io/gorm"
)

type RuConfigRepoImpl struct {
	provider      db.Provider
	defaultConfig *entity.RuConfig
}

func NewRuConfigRepo(provider db.Provider, ruConfig *config.RuConfig) (repo.RuConfigRepo, error) {
	if ruConfig == nil {
		return nil, fmt.Errorf("default Ru Config is nil")
	}

	defaultConfig, err := cvtDefaultRuConfigToEntity(ruConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to convert default Ru Config: %w", err)
	}

	// validate the ruConfig
	if err := isValid(defaultConfig); err != nil {
		return nil, fmt.Errorf("invalid ruConfig: %w", err)
	}

	return &RuConfigRepoImpl{
		provider:      provider,
		defaultConfig: defaultConfig,
	}, nil
}

// GetServerDefaultRuConfig returns the server-default RuConfig
// loaded from YAML at startup. The returned value is a defensive
// deep copy of the shared default struct so callers can mutate the
// result (e.g. fill in the tenant-vs-default priority resolution
// via resolve.PriorityResolveForSharedPool) without affecting
// future GetServerDefaultRuConfig / GetRuConfig calls.
//
// Used by `domain/server/create_sharedpool.go` to resolve the
// tenant's autoscale flag nil-values against the server default at
// shared-pool creation time (see PriorityResolveForSharedPool in
// pkg/autoscale/resolve/resolve.go).
func (r *RuConfigRepoImpl) GetServerDefaultRuConfig(ctx context.Context) (*entity.RuConfig, error) {
	if r.defaultConfig == nil {
		return &entity.RuConfig{}, nil
	}
	return copyRuConfig(r.defaultConfig), nil
}

func (r *RuConfigRepoImpl) GetRuConfig(ctx context.Context, id string, scope entity.RuConfigScope) (*entity.RuConfig, error) {
	ruConfig, found, err := r.GetRuConfigWithFound(ctx, id, scope)
	if err != nil {
		return nil, err
	}
	if !found {
		return r.defaultConfig, nil
	}
	return ruConfig, nil
}

func (r *RuConfigRepoImpl) GetRuConfigWithFound(ctx context.Context, id string, scope entity.RuConfigScope) (*entity.RuConfig, bool, error) {
	if id == "" {
		return r.defaultConfig, false, nil
	}

	var ruConfig entity.RuConfig
	rdb := r.provider.ReadDB(ctx)

	var ruconfigInfoStr string
	if err := rdb.Model(&schema.RuConfig{}).Where("scope = ? AND name = ?", strings.ToLower(scope.String()), id).
		Select("config").First(&ruconfigInfoStr).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return r.defaultConfig, false, nil
		}
		return nil, false, fmt.Errorf("failed to load RU config: %w", err)
	}

	// Unmarshal the ruconfig info
	if ruconfigInfoStr == "" {
		return r.defaultConfig, true, nil
	}

	// old scheduling component name compatibility
	ruconfigInfoStr = strings.ReplaceAll(ruconfigInfoStr, "pd_scheduler", "pd_scheduling")

	if err := protojson.Unmarshal([]byte(ruconfigInfoStr), &ruConfig); err != nil {
		return nil, true, fmt.Errorf("failed to unmarshal ruconfig info: %w", err)
	}

	// fill default optional components for backward compatibility
	fillDefaultOptionalComponents(&ruConfig, r.defaultConfig)

	return &ruConfig, true, nil
}

func (r *RuConfigRepoImpl) SetRuConfig(ctx context.Context, id string, scope entity.RuConfigScope, ruConfig *entity.RuConfig) error {
	if ruConfig == nil {
		return fmt.Errorf("ruConfig is nil")
	}

	// validate the ruConfig
	if err := isValid(ruConfig); err != nil {
		return fmt.Errorf("invalid ruConfig: %w", err)
	}
	wdb := r.provider.WriteDB(ctx)
	ruConfigInfo, err := protojson.Marshal(ruConfig)
	if err != nil {
		return fmt.Errorf("failed to marshal ruConfig: %w", err)
	}

	// Inert or Update RU config
	if err := wdb.Model(&schema.RuConfig{}).Where("scope = ? AND name = ?", strings.ToLower(scope.String()), id).
		Assign(schema.RuConfig{
			Scope:  strings.ToLower(scope.String()),
			Name:   id,
			Config: string(ruConfigInfo),
		}).FirstOrCreate(&schema.RuConfig{}).Error; err != nil {
		return fmt.Errorf("failed to save RU config: %w", err)
	}

	return nil
}

func (r *RuConfigRepoImpl) DeleteRuConfig(ctx context.Context, id string, scope entity.RuConfigScope) error {
	if id == "" {
		return fmt.Errorf("id cannot be empty")
	}

	wdb := r.provider.WriteDB(ctx)

	if err := wdb.Where("scope = ? AND name = ?", strings.ToLower(scope.String()), id).Delete(&schema.RuConfig{}).Error; err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return fmt.Errorf("failed to delete RU config: %w", err)
		}
	}

	return nil
}

// GetFeatureFlags returns the FeatureFlags block of the (scope, id)
// row. If the row does not exist, returns the server-default
// FeatureFlags (which has every field set). If the row exists but
// has FeatureFlags == nil in the JSON, returns an empty struct with
// every pointer nil (= "follow default").
func (r *RuConfigRepoImpl) GetFeatureFlags(ctx context.Context, id string, scope entity.RuConfigScope) (*entity.RuConfigFeatureFlags, error) {
	if id == "" {
		// No row in DB for the empty id — return a copy of the default.
		return r.cloneDefaultFeatureFlags(), nil
	}

	rdb := r.provider.ReadDB(ctx)
	var ruconfigInfoStr string
	if err := rdb.Model(&schema.RuConfig{}).
		Where("scope = ? AND name = ?", strings.ToLower(scope.String()), id).
		Select("config").First(&ruconfigInfoStr).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return r.cloneDefaultFeatureFlags(), nil
		}
		return nil, fmt.Errorf("failed to load RU config for feature flags: %w", err)
	}

	if ruconfigInfoStr == "" {
		return r.cloneDefaultFeatureFlags(), nil
	}

	var row entity.RuConfig
	if err := protojson.Unmarshal([]byte(ruconfigInfoStr), &row); err != nil {
		return nil, fmt.Errorf("failed to unmarshal ruconfig info for feature flags: %w", err)
	}

	if row.FeatureFlags == nil {
		return &entity.RuConfigFeatureFlags{}, nil
	}
	return row.FeatureFlags, nil
}

// cloneDefaultFeatureFlags returns a deep copy of the server-default
// FeatureFlags block. Callers may mutate the returned struct (e.g.
// per-field default resolution), so we must NOT hand out the shared
// r.defaultConfig pointer — doing so would corrupt the in-memory
// default for every subsequent reader.
func (r *RuConfigRepoImpl) cloneDefaultFeatureFlags() *entity.RuConfigFeatureFlags {
	dflt := r.defaultConfig.GetFeatureFlags()
	if dflt == nil {
		return nil
	}
	return proto.Clone(dflt).(*entity.RuConfigFeatureFlags)
}

// ReplaceFeatureFlags writes the given FeatureFlags block to the
// (scope, id) row, REPLACING all 6 autoscale flag values wholesale
// (not merging with the existing row). This is the DENSE
// counterpart to PatchFeatureFlags, used for POST
// CreateOrReplaceAutoscalePolicy and SYNC REPLACE_ALL semantics
// where the canonical bundle is authoritative.
//
// If flags is nil, every autoscale flag is cleared (= all 5 pointers
// are nil = "no override at this scope, fall through to default").
// This is the MVP "wipe overrides" contract for POST `specs: []`.
//
// The row is created (with the given FeatureFlags and the
// server-default everywhere else) if absent. The row's other
// (non-FeatureFlags) fields are left untouched on update.
//
// Dense semantics: a nil pointer in the input is treated as "no
// override at this scope" (column becomes NULL). This is the
// inverse of PatchFeatureFlags's sparse semantics where nil
// means "don't touch the current value" — callers must use the
// right method for the right operation.
func (r *RuConfigRepoImpl) ReplaceFeatureFlags(ctx context.Context, id string, scope entity.RuConfigScope, flags *entity.RuConfigFeatureFlags) error {
	if id == "" {
		return fmt.Errorf("id cannot be empty for ReplaceFeatureFlags")
	}
	if flags == nil {
		// nil flags = "clear all 6 autoscale overrides" = the MVP
		// wipe contract for POST `specs: []`. Use an empty struct
		// (all pointers nil) so protojson marshals to {} and the
		// round-trip preserves nil.
		flags = &entity.RuConfigFeatureFlags{}
	}

	wdb := r.provider.WriteDB(ctx)
	return wdb.Transaction(func(tx *gorm.DB) error {
		// 1. Read current row to preserve non-FeatureFlags fields
		//    (Components, etc.) on update. If the row is absent,
		//    we create one with the given FeatureFlags and
		//    server-default elsewhere.
		var currentJSON string
		err := tx.Model(&schema.RuConfig{}).
			Where("scope = ? AND name = ?", strings.ToLower(scope.String()), id).
			Select("config").First(&currentJSON).Error

		var currentCfg entity.RuConfig
		exists := true
		switch {
		case err == nil && currentJSON != "":
			if err := protojson.Unmarshal([]byte(currentJSON), &currentCfg); err != nil {
				return fmt.Errorf("unmarshal current ruconfig: %w", err)
			}
		case err == nil:
			// Row exists but config is empty — preserve nothing
			// from it, just write our flags.
			currentCfg = entity.RuConfig{
				RuConfig: apiv1.RuConfig{
					FeatureFlags: &entity.RuConfigFeatureFlags{},
					Components:   map[string]*entity.RuConfigComponentsMap{},
				},
			}
		case errors.Is(err, gorm.ErrRecordNotFound):
			// Absent-row path for REPLACE. Build a fresh
			// currentCfg from the server default for the
			// non-feature data (Components + the 2 plain
			// non-autoscale booleans) but keep ALL 6 autoscale
			// optional fields nil. Without the nil autoscale
			// fields, the line 268-276 overwrite below would
			// still emit nils (cloneBoolPtr(nil) == nil), so
			// functionally the persisted row is identical to a
			// "copy with nils" — but the source is now explicit
			// about intent and a future change cannot accidentally
			// re-introduce the bug. The Components fix below is
			// what unblocks the post-#2848 CreateSharedPool path
			// (isValid rejects rows with empty Components).
			exists = false
			currentCfg = newRowFromServerDefaultForReplace(r.defaultConfig)
		default:
			return fmt.Errorf("load ruconfig for replace: %w", err)
		}

		// 2. Replace (not merge) the FeatureFlags block. The
		//    caller's flags carry the 6 autoscale optional
		//    fields; the 2 non-autoscale plain-bool fields
		//    (EnableTici, EnableResourceManager) are loaded
		//    from the existing row above and carried through
		//    verbatim. Without this, a POST / SYNC REPLACE_ALL
		//    policy write would silently clear tici /
		//    resource_manager.
		if currentCfg.FeatureFlags == nil {
			currentCfg.FeatureFlags = &entity.RuConfigFeatureFlags{}
		}
		preserveTici := currentCfg.FeatureFlags.EnableTici
		preserveResourceManager := currentCfg.FeatureFlags.EnableResourceManager
		currentCfg.FeatureFlags = &entity.RuConfigFeatureFlags{
			EnableTici:                          preserveTici,
			EnableResourceManager:               preserveResourceManager,
			EnableScaleSharedPoolByRu:           cloneBoolPtr(flags.EnableScaleSharedPoolByRu),
			EnableScaleTidbGroupByRu:            cloneBoolPtr(flags.EnableScaleTidbGroupByRu),
			EnableAutoscaleTikvCpuScaleOut:      cloneBoolPtr(flags.EnableAutoscaleTikvCpuScaleOut),
			EnableAutoscaleDiskExpand:           cloneBoolPtr(flags.EnableAutoscaleDiskExpand),
			EnableAutoscaleIopsThroughputExpand: cloneBoolPtr(flags.EnableAutoscaleIopsThroughputExpand),
			EnableAutoscaleTiproxyScaleIn:       cloneBoolPtr(flags.EnableAutoscaleTiproxyScaleIn),
		}

		marshaled, err := protojson.Marshal(&currentCfg)
		if err != nil {
			return fmt.Errorf("marshal replaced ruconfig: %w", err)
		}

		row := schema.RuConfig{
			Scope:  strings.ToLower(scope.String()),
			Name:   id,
			Config: string(marshaled),
		}
		if exists {
			if err := tx.Model(&schema.RuConfig{}).
				Where("scope = ? AND name = ?", row.Scope, row.Name).
				Updates(map[string]any{"config": row.Config}).Error; err != nil {
				return fmt.Errorf("update ruconfig for replace: %w", err)
			}
		} else {
			if err := tx.Create(&row).Error; err != nil {
				return fmt.Errorf("create ruconfig for replace: %w", err)
			}
		}
		return nil
	})
}

// PatchFeatureFlags merges the per-field patch into the (scope, id)
// row's FeatureFlags block, leaving every other field of the row
// untouched. A nil pointer in the patch means "don't touch this field"
// (preserve current). Non-nil means "set this field to the given
// value".
//
// NOTE: nil currently means "preserve", not "clear" — the
// FeatureFlagsPatch struct uses a single *bool per field and cannot
// distinguish "not in the patch" from "explicitly clear". If the
// policy-shaped PATCH RPCs later need a clear override operation,
// this API will need to be extended.
//
// If the row is absent, it is created with the patched FeatureFlags
// and server-default everywhere else. If HasAnyChange() is false,
// the row is not touched.
func (r *RuConfigRepoImpl) PatchFeatureFlags(ctx context.Context, id string, scope entity.RuConfigScope, patch repo.FeatureFlagsPatch) error {
	if id == "" {
		return fmt.Errorf("id cannot be empty for PatchFeatureFlags")
	}
	if !patch.HasAnyChange() {
		// No-op: caller asked for a patch with all-nil pointers.
		return nil
	}

	wdb := r.provider.WriteDB(ctx)
	return wdb.Transaction(func(tx *gorm.DB) error {
		// 1. Read current row (or default).
		var currentJSON string
		err := tx.Model(&schema.RuConfig{}).
			Where("scope = ? AND name = ?", strings.ToLower(scope.String()), id).
			Select("config").First(&currentJSON).Error

		var currentCfg entity.RuConfig
		exists := true
		switch {
		case err == nil && currentJSON != "":
			if err := protojson.Unmarshal([]byte(currentJSON), &currentCfg); err != nil {
				return fmt.Errorf("unmarshal current ruconfig: %w", err)
			}
		case err == nil:
			// Row exists but config is empty — treat as default.
			currentCfg = entity.RuConfig{
				RuConfig: apiv1.RuConfig{
					FeatureFlags: copyFeatureFlags(r.defaultConfig.GetFeatureFlags()),
					Components:   map[string]*entity.RuConfigComponentsMap{},
				},
			}
		case errors.Is(err, gorm.ErrRecordNotFound):
			// Absent-row path for PATCH. Build a fresh
			// currentCfg from the server default for the
			// non-feature data (Components + the 2 plain
			// non-autoscale booleans) but keep ALL 6 autoscale
			// optional fields nil. The merge contract: nil
			// autoscale fields in the fresh row + mergeFeatureFlags
			// means a sparse PATCH (1 of 5 fields set) lands only
			// the set fields — the unset fields stay nil,
			// preserving the proper "tenant did not set" state.
			// Otherwise a sparse PATCH would silently freeze the
			// un-set 4 fields to a copy of the server default at
			// PATCH-time, preventing future PATCHes from ever
			// overriding them.
			exists = false
			currentCfg = newRowFromServerDefaultForPatch(r.defaultConfig)
		default:
			return fmt.Errorf("load ruconfig for patch: %w", err)
		}

		// 2. Merge the patch into FeatureFlags. nil = explicit NULL
		//    (clear override), non-nil = set the value.
		merged := mergeFeatureFlags(currentCfg.GetFeatureFlags(), patch)

		// 3. Apply.
		if currentCfg.FeatureFlags == nil {
			currentCfg.FeatureFlags = &entity.RuConfigFeatureFlags{}
		}
		currentCfg.FeatureFlags = merged

		marshaled, err := protojson.Marshal(&currentCfg)
		if err != nil {
			return fmt.Errorf("marshal patched ruconfig: %w", err)
		}

		row := schema.RuConfig{
			Scope:  strings.ToLower(scope.String()),
			Name:   id,
			Config: string(marshaled),
		}
		if exists {
			if err := tx.Model(&schema.RuConfig{}).
				Where("scope = ? AND name = ?", row.Scope, row.Name).
				Updates(map[string]any{"config": row.Config}).Error; err != nil {
				return fmt.Errorf("update ruconfig for patch: %w", err)
			}
		} else {
			if err := tx.Create(&row).Error; err != nil {
				return fmt.Errorf("create ruconfig for patch: %w", err)
			}
		}
		return nil
	})
}

// copyFeatureFlags returns a deep copy of ff so the caller can mutate
// the result without affecting the server-default shared struct.
//
// Important: this copies ALL 8 fields of FeatureFlags, including the
// 2 non-autoscale plain-bool fields (EnableTici, EnableResourceManager).
// The policy-shaped write paths (PatchFeatureFlags, ReplaceFeatureFlags)
// only modify the 6 autoscale optional fields, so copyFeatureFlags is
// the one place that must remember to carry EnableTici and
// EnableResourceManager through — silently dropping them would clear
// tici / resource_manager for every policy write.
func copyFeatureFlags(ff *entity.RuConfigFeatureFlags) *entity.RuConfigFeatureFlags {
	if ff == nil {
		return &entity.RuConfigFeatureFlags{}
	}
	out := &entity.RuConfigFeatureFlags{
		EnableTici:                          ff.EnableTici,
		EnableResourceManager:               ff.EnableResourceManager,
		EnableScaleSharedPoolByRu:           cloneBoolPtr(ff.EnableScaleSharedPoolByRu),
		EnableScaleTidbGroupByRu:            cloneBoolPtr(ff.EnableScaleTidbGroupByRu),
		EnableAutoscaleTikvCpuScaleOut:      cloneBoolPtr(ff.EnableAutoscaleTikvCpuScaleOut),
		EnableAutoscaleDiskExpand:           cloneBoolPtr(ff.EnableAutoscaleDiskExpand),
		EnableAutoscaleIopsThroughputExpand: cloneBoolPtr(ff.EnableAutoscaleIopsThroughputExpand),
		EnableAutoscaleTiproxyScaleIn:       cloneBoolPtr(ff.EnableAutoscaleTiproxyScaleIn),
	}
	return out
}

// newRowFromServerDefaultForReplace builds the absent-row
// baseline for ReplaceFeatureFlags. The result has:
//   - Components: a deep copy of the server default's Components map,
//     so a fresh row has full component RU ranges and the
//     isValid check on the next CreateSharedPool / SetRuConfig
//     doesn't reject it as "component has no RU ranges defined".
//   - FeatureFlags.EnableTici / EnableResourceManager: copied
//     from the server default (the 2 plain-bool fields that
//     aren't part of the proto3-`optional` autoscale set).
//   - The 6 autoscale optional bool fields: ALL nil. The line
//     268-276 overwrite below clones the input's 5 fields
//     (cloneBoolPtr preserves nil) into these, so the persisted
//     row is exactly the input's 5 fields — no accidental
//     inheritance of the server default.
func newRowFromServerDefaultForReplace(serverDefault *entity.RuConfig) entity.RuConfig {
	return entity.RuConfig{
		RuConfig: apiv1.RuConfig{
			FeatureFlags: &entity.RuConfigFeatureFlags{
				EnableTici:            serverDefault.GetFeatureFlags().GetEnableTici(),
				EnableResourceManager: serverDefault.GetFeatureFlags().GetEnableResourceManager(),
				// 6 autoscale optional fields intentionally nil
			},
			Components: deepCopyComponents(serverDefault.GetComponents()),
		},
	}
}

// newRowFromServerDefaultForPatch builds the absent-row baseline
// for PatchFeatureFlags. Same shape as the REPLACE variant for
// Components and the 2 plain booleans; the difference is
// semantic intent:
//
//   - For REPLACE: the caller is providing a dense autoscale
//     block; the line 268-276 overwrite produces a row whose 5
//     autoscale fields = exactly the input.
//
//   - For PATCH: the caller is providing a SPARSE block; the
//     line 364 mergeFeatureFlags call merges the patch onto
//     the baseline's autoscale fields. With the 6 autoscale
//     fields nil in the baseline, the merge produces: the
//     patch's set fields become non-nil, the unset fields stay
//     nil. This is the correct PATCH semantic (a one-key
//     PATCH must NOT freeze the other 4 to the server default
//     — the next PATCH must still be able to override them).
func newRowFromServerDefaultForPatch(serverDefault *entity.RuConfig) entity.RuConfig {
	return newRowFromServerDefaultForReplace(serverDefault)
}

// deepCopyComponents returns a deep copy of the input
// Components map (RuConfigComponentsMap values + each map's
// NodeSpecRuRange map + each RuRange struct). Used by the
// absent-row paths in PatchFeatureFlags / ReplaceFeatureFlags
// to seed a fresh row with the server default's component
// config. nil-safe (returns nil for nil input).
func deepCopyComponents(in map[string]*entity.RuConfigComponentsMap) map[string]*entity.RuConfigComponentsMap {
	if in == nil {
		return nil
	}
	out := make(map[string]*entity.RuConfigComponentsMap, len(in))
	for k, v := range in {
		if v == nil {
			out[k] = nil
			continue
		}
		clonedRange := make(map[string]*entity.RuRange, len(v.GetNodeSpecRuRange()))
		for rk, rv := range v.GetNodeSpecRuRange() {
			if rv == nil {
				clonedRange[rk] = nil
				continue
			}
			clonedRange[rk] = &entity.RuRange{
				Min: rv.GetMin(),
				Max: rv.GetMax(),
			}
		}
		out[k] = &entity.RuConfigComponentsMap{
			MaxNodeCount:    v.GetMaxNodeCount(),
			NodeSpecRuRange: clonedRange,
		}
	}
	return out
}

// copyRuConfig returns a defensive deep copy of the input RuConfig
// so the caller (e.g. GetServerDefaultRuConfig) can hand out a value
// that the receiver is free to mutate without affecting the source
// struct. Components map is copied element-by-element; each
// ComponentsMap's NodeSpecRuRange is also deep-copied.
func copyRuConfig(in *entity.RuConfig) *entity.RuConfig {
	if in == nil {
		return &entity.RuConfig{}
	}
	out := &entity.RuConfig{
		RuConfig: apiv1.RuConfig{
			FeatureFlags: copyFeatureFlags(in.GetFeatureFlags()),
			Components:   make(map[string]*entity.RuConfigComponentsMap, len(in.GetComponents())),
		},
	}
	for k, v := range in.GetComponents() {
		if v == nil {
			out.Components[k] = nil
			continue
		}
		clonedRange := make(map[string]*entity.RuRange, len(v.GetNodeSpecRuRange()))
		for rk, rv := range v.GetNodeSpecRuRange() {
			if rv == nil {
				clonedRange[rk] = nil
				continue
			}
			clonedRange[rk] = &entity.RuRange{
				Min: rv.GetMin(),
				Max: rv.GetMax(),
			}
		}
		out.Components[k] = &entity.RuConfigComponentsMap{
			MaxNodeCount:    v.GetMaxNodeCount(),
			NodeSpecRuRange: clonedRange,
		}
	}
	return out
}

func cloneBoolPtr(b *bool) *bool {
	if b == nil {
		return nil
	}
	v := *b
	return &v
}

// mergeFeatureFlags returns a new FeatureFlags where the patch's
// non-nil fields are applied (overwrite the current value) and the
// patch's nil fields preserve the current value (= "don't touch").
// mergeFeatureFlags returns a new FeatureFlags where the patch's
// non-nil fields are applied (overwrite the current value) and the
// patch's nil fields preserve the current value (= "don't touch").
//
// Unlike a future "explicit clear" API where nil would mean
// "clear this override", the current implementation uses nil as
// "not specified in the patch" because the FeatureFlagsPatch
// struct cannot distinguish between the two intents with a single
// *bool per field. To remove a single override, callers POST the
// desired final state (the MVP "wipe overrides" contract: POST
// `specs: []` clears every autoscale flag at the row).
// TODO: add explicit clear support via a separate mechanism (e.g.
// dedicated Clear* bool fields) when the policy-shaped PATCH RPC
// needs it.
//
// The 6 autoscale optional fields go through applyPatchField (sparse
// merge). The 2 non-autoscale plain-bool fields (EnableTici,
// EnableResourceManager) are NOT in the FeatureFlagsPatch struct
// at all — policy writes cannot touch them, and the existing row's
// values are carried through verbatim. Dropping them here would
// silently clear tici / resource_manager for every PATCH.
func mergeFeatureFlags(current *entity.RuConfigFeatureFlags, patch repo.FeatureFlagsPatch) *entity.RuConfigFeatureFlags {
	if current == nil {
		current = &entity.RuConfigFeatureFlags{}
	}
	out := &entity.RuConfigFeatureFlags{
		EnableTici:                          current.EnableTici,
		EnableResourceManager:               current.EnableResourceManager,
		EnableScaleSharedPoolByRu:           applyPatchField(current.EnableScaleSharedPoolByRu, patch.EnableScaleSharedPoolByRu),
		EnableScaleTidbGroupByRu:            applyPatchField(current.EnableScaleTidbGroupByRu, patch.EnableScaleTidbGroupByRu),
		EnableAutoscaleTikvCpuScaleOut:      applyPatchField(current.EnableAutoscaleTikvCpuScaleOut, patch.EnableAutoscaleTikvCpuScaleOut),
		EnableAutoscaleDiskExpand:           applyPatchField(current.EnableAutoscaleDiskExpand, patch.EnableAutoscaleDiskExpand),
		EnableAutoscaleIopsThroughputExpand: applyPatchField(current.EnableAutoscaleIopsThroughputExpand, patch.EnableAutoscaleIopsThroughputExpand),
		EnableAutoscaleTiproxyScaleIn:       applyPatchField(current.EnableAutoscaleTiproxyScaleIn, patch.EnableAutoscaleTiproxyScaleIn),
	}
	return out
}

// applyPatchField applies a single field from the patch onto the
// current value. If patchField is nil, the field was not specified
// in the patch and the current value is preserved. If patchField is
// non-nil, the current value is overwritten with a copy of the
// patch's value.
func applyPatchField(current, patchField *bool) *bool {
	if patchField == nil {
		// Caller did not specify this field in the patch → preserve current.
		return current
	}
	v := *patchField
	return &v
}

func cvtDefaultRuConfigToEntity(cfg *config.RuConfig) (*entity.RuConfig, error) {
	if cfg == nil {
		return nil, fmt.Errorf("RU config is nil")
	}

	ruCfg := &entity.RuConfig{
		RuConfig: apiv1.RuConfig{
			FeatureFlags: &entity.RuConfigFeatureFlags{
				// The 6 autoscale flags are *bool (proto3 optional) so
				// tenant / sharedpool can distinguish "not set" (nil,
				// falls through to server default) from "explicitly
				// false" (overrides server default). Server config is
				// always "set" (loaded from YAML with struct-tag
				// defaults), so we always wrap the bool with & to
				// produce a non-nil *bool. priorityResolveForSharedPool
				// will use these as the fallback when a tenant/sharedpool
				// field is nil.
				EnableScaleSharedPoolByRu:           &cfg.EnableScaleSharedPoolByRu,
				EnableScaleTidbGroupByRu:            &cfg.EnableScaleTiDBGroupByRu,
				EnableAutoscaleTikvCpuScaleOut:      &cfg.EnableAutoscaleTikvCpuScaleOut,
				EnableAutoscaleDiskExpand:           &cfg.EnableAutoscaleDiskExpand,
				EnableAutoscaleIopsThroughputExpand: &cfg.EnableAutoscaleIopsThroughputExpand,
				EnableAutoscaleTiproxyScaleIn:       &cfg.EnableAutoscaleTiproxyScaleIn,
			},
			Components: make(map[string]*entity.RuConfigComponentsMap),
		},
	}

	for component, componentCfg := range cfg.Components {
		if componentCfg == nil {
			continue
		}

		// old scheduling component name compatibility
		if component == "pd_scheduler" {
			component = "pd_scheduling"
		}

		ruCfg.Components[component] = &entity.RuConfigComponentsMap{
			NodeSpecRuRange: make(map[string]*entity.RuRange),
		}
		for nodeSpec, ruRange := range componentCfg {
			ruCfg.Components[component].NodeSpecRuRange[nodeSpec] = &entity.RuRange{
				Min:              ruRange.Min,
				Max:              ruRange.Max,
				DefaultNodeCount: ruRange.DefaultNodeCount,
			}
		}
	}

	for component, nodeMax := range cfg.NodeCountQuota {

		// old scheduling component name compatibility
		if component == "pd_scheduler" {
			component = "pd_scheduling"
		}
		if _, ok := ruCfg.Components[component]; !ok {
			ruCfg.Components[component] = &entity.RuConfigComponentsMap{
				NodeSpecRuRange: make(map[string]*entity.RuRange),
			}
		}
		ruCfg.Components[component].MaxNodeCount = nodeMax
	}

	return ruCfg, nil
}
