package server

import (
	"context"
	"fmt"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	specpkg "github.com/tidbcloud/cluster-service-ng/pkg/autoscale/spec"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	autoscalingv2 "k8s.io/api/autoscaling/v2"
	"k8s.io/apimachinery/pkg/labels"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// readRuntimeSpecs returns the runtime policy specs for a given
// (scope, subjectID). For SHARED_POOL and CLUSTER this is the LIVE
// state read directly from the two storage points of truth:
// HPA `spec.behavior` (K8s) and ru_configs.FeatureFlags (DB).
// K8s/DB errors propagate (no silent fallback to the dormant
// autoscale_policies table). DEFAULT and TENANT return nil — they
// have no runtime state. See design doc §Inheritance Chains.
func (s *serverImpl) readRuntimeSpecs(
	ctx context.Context,
	scope apiv1.AutoscalePolicyScope,
	subjectID string,
) ([]*entity.AutoscalePolicySpec, error) {
	switch scope {
	case apiv1.AutoscalePolicyScope_SCOPE_SHARED_POOL:
		return s.readRuntimeSpecsForSharedPool(ctx, subjectID)
	case apiv1.AutoscalePolicyScope_SCOPE_TIDB_GROUP:
		return s.readRuntimeSpecsForCluster(ctx, subjectID)
	default:
		return nil, nil
	}
}

// readRuntimeSpecsForSharedPool returns the live runtime specs for a
// shared pool: HPA-derived rows for the 3 worker types plus TIKV
// SCALE_OUT rows from ru_configs.FeatureFlags. HPA errors and
// FeatureFlags read errors both propagate to the caller.
func (s *serverImpl) readRuntimeSpecsForSharedPool(ctx context.Context, spID string) ([]*entity.AutoscalePolicySpec, error) {
	if spID == "" {
		return nil, nil
	}
	var specs []*entity.AutoscalePolicySpec
	// HPA-derived rows (3 worker types). TIKV has no HPA
	// in this codebase — its SCALE_OUT rows come from FeatureFlags
	// below. Empty groupID = SHARED_POOL scope → list every HPA.
	if s.sharedPoolRepo != nil && s.dataplaneManager != nil {
		hpaSpecs, err := s.listSharedPoolHPASpecs(ctx, spID, "", "")
		if err != nil {
			return nil, status.Errorf(codes.Internal,
				"read HPA for shared pool %s: %v", spID, err)
		}
		specs = append(specs, hpaSpecs...)
	}
	if s.ruConfigRepo != nil {
		// GetRuConfig returns the server default (non-nil, nil error)
		// when the row is absent, so any error here is a real DB /
		// unmarshal failure. Propagate it instead of swallowing.
		// Even when rc.FeatureFlags is nil (ru_config row absent or
		// FeatureFlags column never written), still call
		// featureFlagsToSpecs — it emits 9 DISABLED fallback specs
		// so the bundle is complete and the operator can see what's
		// there. See featureFlagsToSpecs godoc for the two
		// nil cases and their semantics.
		rc, err := s.ruConfigRepo.GetRuConfig(ctx, spID, apiv1.RuConfig_SHARED_POOL)
		if err != nil {
			return nil, status.Errorf(codes.Internal,
				"read ru config for shared pool %s: %v", spID, err)
		}
		if rc != nil {
			specs = append(specs, featureFlagsToSpecs(rc.FeatureFlags)...)
		}
	}
	return specs, nil
}

// readRuntimeSpecsForCluster returns cluster-specific runtime specs:
// TIDB + TIPROXY HPA-derived rows. TIKV SCALE_OUT rows from
// FeatureFlags are NOT returned (shared-pool-level, resolved via
// the priority chain at chainEffectiveSpecs time, not as a CLUSTER
// override). The HPA read is scoped by groupID (shared pools may
// host multiple TiDB groups); the K8s-side selector matches
// SetHPAScaleBehavior's write path. If the cluster's tidbGroup
// cannot be loaded or has no shared_pool_id, returns nil with no
// error — CLUSTER scope is effectively empty until the cluster
// joins a shared pool.
func (s *serverImpl) readRuntimeSpecsForCluster(ctx context.Context, groupID string) ([]*entity.AutoscalePolicySpec, error) {
	if s.tidbGroupRepo == nil {
		return nil, nil
	}
	tg, err := s.tidbGroupRepo.Load(ctx, groupID, false)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "load tidb group %s: %v", groupID, err)
	}
	if tg == nil {
		return nil, nil
	}
	spID := tg.GetSharedPoolId()
	if spID == "" {
		return nil, nil
	}

	var specs []*entity.AutoscalePolicySpec
	// HPA-derived rows scoped to this group. Passing groupID
	// switches listSharedPoolHPASpecs into the group-scoped path.
	// clusterID is derived from the loaded tidbGroup — per-group
	// TIDB HPA's group label is `db-<groupID>`, but per-group TIPROXY
	// HPA's group label is `db-<clusterID>` (created at
	// generatedTiProxyGroup with comp.Id = tidbCluster.GetId()).
	// Passing clusterID lets listSharedPoolHPASpecs build the right
	// selector for TIPROXY.
	clusterID := tg.GetClusterId()
	if s.sharedPoolRepo != nil && s.dataplaneManager != nil {
		hpaSpecs, err := s.listSharedPoolHPASpecs(ctx, spID, clusterID, groupID)
		if err != nil {
			return nil, status.Errorf(codes.Internal,
				"read HPA for cluster %s in shared pool %s: %v", groupID, spID, err)
		}
		specs = append(specs, hpaSpecs...)
	}

	out := make([]*entity.AutoscalePolicySpec, 0, len(specs))
	for _, sp := range specs {
		if sp == nil {
			continue
		}
		ct := sp.GetTargetType()
		// CLUSTER scope only carries TIDB + TIPROXY; TIKV rows
		// are shared-pool-level (resolved via the chain).
		if ct != apiv1.AutoscaleTargetType_TARGET_TIDB && ct != apiv1.AutoscaleTargetType_TARGET_TIPROXY {
			continue
		}
		out = append(out, sp)
	}
	return out, nil
}

// listSharedPoolHPASpecs lists HPAs in the sharedpool's namespace and
// returns the derived AutoscalePolicySpec list. Returns empty (non-nil)
// + nil error if the namespace has no HPAs yet (implicit-allow for
// the 3 scale entrypoints; HPA-still-being-created transient for the
// operator RPC).
//
// groupID empty (SHARED_POOL scope): every HPA in the namespace —
// pool-level scope, workers live at the shared-pool level.
// groupID non-empty (CLUSTER scope): only HPAs scoped to the
// requesting group, with the same selector SetHPAScaleBehavior uses
// on the write path. This prevents cross-group leakage in shared
// pools hosting multiple TiDB groups; the K8s-side selector matches
// the write path so a future CLUSTER-filter change cannot
// reintroduce leakage.
func (s *serverImpl) listSharedPoolHPASpecs(
	ctx context.Context,
	spID, clusterID, groupID string,
) ([]*entity.AutoscalePolicySpec, error) {
	if spID == "" {
		return nil, nil
	}
	sp, err := s.sharedPoolRepo.Load(ctx, spID, false)
	if err != nil {
		return nil, fmt.Errorf("load shared pool %s: %w", spID, err)
	}
	if sp == nil {
		return nil, fmt.Errorf("shared pool %s not found", spID)
	}
	remoteCli, err := s.dataplaneManager.GetClient(sp.GetDataplaneName())
	if err != nil {
		return nil, fmt.Errorf("get dataplane client for %s: %w", sp.GetDataplaneName(), err)
	}
	namespace := "tidb" + sp.GetId()

	var hpas []autoscalingv2.HorizontalPodAutoscaler
	if groupID == "" {
		// SHARED_POOL scope: per-component-type List with label
		// selectors, mirroring the CLUSTER branch below. Never include
		// per-group TIDB/TIPROXY HPAs (CLUSTER-only); only the
		// pool-level worker HPAs and, if the pool has one, the
		// pool-shared TIPROXY HPA (see design doc §Pool-shared TIPROXY).
		poolScoped := []apiv1.ComponentType{
			apiv1.ComponentType_TIDB_WORKER,
			apiv1.ComponentType_TIKV_WORKER,
			apiv1.ComponentType_COPROCESSOR_WORKER,
		}
		if entity.HasSharedTiProxy(sp) {
			poolScoped = append(poolScoped, apiv1.ComponentType_TIPROXY)
		}
		for _, ct := range poolScoped {
			labelValue := componentLabelForHPA(ct)
			if labelValue == "" {
				continue
			}
			selectorSet := map[string]string{
				schemav1.LabelComponentKey: labelValue,
			}
			if ct == apiv1.ComponentType_TIPROXY {
				// Pool-shared TIPROXY HPA label is `db-<FnvHash(spID)>`
				// (created at shared-pool create time, see
				// deployments/templates/tiproxy_group_hpa.templ and
				// domain/server/gen_component.go).
				selectorSet[schemav1.LabelPingcapGroupKey] = entity.GetComponentGroupName(&entity.Component{
					Id:            utils.FnvHash(sp.GetId()),
					ComponentType: ct,
				})
			}
			list := new(autoscalingv2.HorizontalPodAutoscalerList)
			if err := remoteCli.List(ctx, list, &client.ListOptions{
				Namespace:     namespace,
				LabelSelector: labels.SelectorFromSet(selectorSet),
			}); err != nil {
				return nil, fmt.Errorf("list %s HPAs in namespace %s: %w", labelValue, namespace, err)
			}
			hpas = append(hpas, list.Items...)
		}
	} else {
		// CLUSTER scope: only TIDB and TIPROXY are per-group
		// HPA components. Workers (TIDB_WORKER / TIKV_WORKER /
		// COPROCESSOR_WORKER) belong to SHARED_POOL scope —
		// their HPA is per-sharedpool, not per-cluster.
		// readRuntimeSpecsForCluster filters non-TIDB/non-TIPROXY
		// anyway; keeping the list scoped avoids wasted K8s API
		// calls. Selector matches SetHPAScaleBehavior's write path
		// — see domain/server/set_hpa_scale_behavior.go.
		//
		// Component identity model (must mirror apply_hpa.go /
		// generatedTiDBGroup / generatedTiProxyGroup):
		//   TIDB    HPA group label = `db-<tidbGroupID>`  → use groupID
		//   TIPROXY HPA group label = `db-<tidbClusterID>` → use clusterID
		for _, ct := range []apiv1.ComponentType{
			apiv1.ComponentType_TIDB,
			apiv1.ComponentType_TIPROXY,
		} {
			labelValue := componentLabelForHPA(ct)
			if labelValue == "" {
				continue
			}
			selectorSet := map[string]string{
				schemav1.LabelComponentKey: labelValue,
			}
			if componentUsesGroupScopedHPA(ct) {
				idForLabel := groupID
				if ct == apiv1.ComponentType_TIPROXY {
					idForLabel = clusterID
				}
				selectorSet[schemav1.LabelPingcapGroupKey] = entity.GetComponentGroupName(&entity.Component{
					Id:            idForLabel,
					ComponentType: ct,
				})
			}
			list := new(autoscalingv2.HorizontalPodAutoscalerList)
			if err := remoteCli.List(ctx, list, &client.ListOptions{
				Namespace:     namespace,
				LabelSelector: labels.SelectorFromSet(selectorSet),
			}); err != nil {
				return nil, fmt.Errorf("list %s HPAs in namespace %s: %w", labelValue, namespace, err)
			}
			hpas = append(hpas, list.Items...)
		}
	}

	specs := make([]*entity.AutoscalePolicySpec, 0, len(hpas))
	for i := range hpas {
		hpa := &hpas[i]
		componentLabel := hpa.GetLabels()[schemav1.LabelComponentKey]
		ct, ok := componentTypeLookup[componentLabel]
		if !ok {
			continue
		}
		specs = append(specs, hpaToSpecs(ct, hpa)...)
	}
	return specs, nil
}

// hpaToSpecs derives AutoscalePolicySpec rows for a single HPA.
// Per-component rules (see pkg/autoscale/hpa_behavior for the
// mirrored translator and the design doc §Inheritance Chains):
//
//   - TIKV: 0 rows (no HPA in this codebase; comes from FeatureFlags).
//   - TIDB / TIPROXY: 2 rows — SCALE_OUT (NODE_COUNT, CPU) from
//     ScaleUp.SelectPolicy, SCALE_IN (NODE_COUNT, CPU) from
//     ScaleDown.SelectPolicy. Both are HPA-driven; the (capability,
//     trigger) tuple is the same in both directions at CLUSTER scope
//     (direction distinguishes them in chain resolution).
//   - TIDB_WORKER / TIKV_WORKER / COPROCESSOR_WORKER: 2 rows —
//     SCALE_OUT from ScaleUp, SCALE_IN from ScaleDown (both HPA-driven).
//   - Other: 0 rows.
//
// Behavior == nil → 0 rows ("no policy in scope" = implicit allow).
func hpaToSpecs(componentType apiv1.ComponentType, hpa *autoscalingv2.HorizontalPodAutoscaler) []*entity.AutoscalePolicySpec {
	if hpa == nil || hpa.Spec.Behavior == nil {
		return nil
	}
	if componentType == apiv1.ComponentType_TIKV {
		return nil
	}
	switch componentType {
	case apiv1.ComponentType_TIDB, apiv1.ComponentType_TIPROXY:
		// Per-group: SCALE_OUT (NODE_COUNT, CPU) from ScaleUp,
		// SCALE_IN (NODE_COUNT, CPU) from ScaleDown. Same (capability,
		// trigger) in both directions at CLUSTER scope — `direction`
		// splits them in chain resolution.
		var specs []*entity.AutoscalePolicySpec
		if hpa.Spec.Behavior.ScaleUp != nil || hpa.Spec.Behavior.ScaleDown != nil {
			specs = append(specs,
				&entity.AutoscalePolicySpec{
					TargetType: specpkg.ComponentTypeToAutoscaleTargetType(componentType),
					Direction:  apiv1.AutoscalePolicy_SCALE_OUT,
					Capability: apiv1.AutoscalePolicy_NODE_COUNT,
					Trigger:    apiv1.AutoscalePolicy_CPU,
					State:      policyStateFromScalingRules(hpa.Spec.Behavior.ScaleUp),
				},
				&entity.AutoscalePolicySpec{
					TargetType: specpkg.ComponentTypeToAutoscaleTargetType(componentType),
					Direction:  apiv1.AutoscalePolicy_SCALE_IN,
					Capability: apiv1.AutoscalePolicy_NODE_COUNT,
					Trigger:    apiv1.AutoscalePolicy_CPU,
					State:      policyStateFromScalingRules(hpa.Spec.Behavior.ScaleDown),
				},
			)
		}
		return specs
	case apiv1.ComponentType_TIDB_WORKER,
		apiv1.ComponentType_TIKV_WORKER,
		apiv1.ComponentType_COPROCESSOR_WORKER:
		// Workers: SCALE_OUT (NODE_COUNT, CPU) from ScaleUp, SCALE_IN
		// (NODE_COUNT, CPU) from ScaleDown — both HPA-driven, same shape
		// as TIDB/TIPROXY. Worker scale-out is written to HPA behavior by
		// SetHPAScaleBehavior/ApplyComponentHpaAction, not via a
		// FeatureFlags flag.
		var specs []*entity.AutoscalePolicySpec
		if hpa.Spec.Behavior.ScaleUp != nil || hpa.Spec.Behavior.ScaleDown != nil {
			specs = append(specs,
				&entity.AutoscalePolicySpec{
					TargetType: specpkg.ComponentTypeToAutoscaleTargetType(componentType),
					Direction:  apiv1.AutoscalePolicy_SCALE_OUT,
					Capability: apiv1.AutoscalePolicy_NODE_COUNT,
					Trigger:    apiv1.AutoscalePolicy_CPU,
					State:      policyStateFromScalingRules(hpa.Spec.Behavior.ScaleUp),
				},
				&entity.AutoscalePolicySpec{
					TargetType: specpkg.ComponentTypeToAutoscaleTargetType(componentType),
					Direction:  apiv1.AutoscalePolicy_SCALE_IN,
					Capability: apiv1.AutoscalePolicy_NODE_COUNT,
					Trigger:    apiv1.AutoscalePolicy_CPU,
					State:      policyStateFromScalingRules(hpa.Spec.Behavior.ScaleDown),
				},
			)
		}
		return specs
	default:
		return nil
	}
}

// policyStateFromScalingRules mirrors
// pkg/autoscale/hpa_behavior::policyStateFromHPA but operates on
// the K8s autoscaling/v2.HPAScalingRules type directly — the proto
// translation in toPBHPAScalingRules strips SelectPolicy, so this
// must read the K8s type before that strip. Omitted SelectPolicy
// defaults to MaxPolicySelect (HPA engaged); only DisabledPolicySelect
// flips state to DISABLED.
func policyStateFromScalingRules(rules *autoscalingv2.HPAScalingRules) apiv1.AutoscalePolicy_State {
	if rules == nil || rules.SelectPolicy == nil {
		return apiv1.AutoscalePolicy_ENABLED
	}
	if *rules.SelectPolicy == autoscalingv2.DisabledPolicySelect {
		return apiv1.AutoscalePolicy_DISABLED
	}
	return apiv1.AutoscalePolicy_ENABLED
}
