package db

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"sort"
	"strings"

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

// 20260718001: extend the DEFAULT scope autoscale policy bundle seed
// from 14 to 18 rows, adding the four SHARED_POOL / TIDB_GROUP logical
// component rows at (NODE_SPEC, RCU).
//
// 20260713002 omitted these four rows. Without them:
//   - SpecsToFeatureFlagsBlock produces nil pointers for
//     EnableScaleSharedPoolByRu / EnableScaleTidbGroupByRu on the
//     DEFAULT-scope bundle.
//   - PriorityResolveForSharedPoolFromBundle falls through to
//     server-default (false) instead of hitting a seeded value.
//
// This is a semantic completeness fix, NOT a behavior change: every
// deployment that runs this migration already has server-default =
// false for both flags, so the chain still resolves to false — the
// difference is that after the seed, DEFAULT has an explicit value
// rather than nil.
//
// All four rows are seeded State=DISABLED per design doc §DEFAULT
// Scope Specifics ("only Workers and TIDB enable SCALE OUT;
// everything else DISABLE"); SHARED_POOL / TIDB_GROUP are neither
// Workers nor TIDB.
//
// Self-contained: seeds all 18 rows so this migration does not
// depend on 20260713002 having run. Cross-environment consistency
// is preserved regardless of migration history.
//
// Re-runnable: ON DUPLICATE KEY UPDATE bumps updated_at AND
// rewrites specs_json + bundle_hash. Already-shipped clusters that
// ran 02 will see their DEFAULT row updated in place from 14 to 18
// rows; the new bundle_hash will differ from the previously-stored
// hash, so the runtime SYNC KEYS_ONLY change-detection path will
// flag the row as stale — this is expected and correct.
//
// bundle_hash is computed via the same hashSeedSpecs algorithm that
// 20260713002 inlined. A follow-up PR will extract this function to
// pkg/autoscale/bundlehash so the migration and runtime
// (domain/server/autoscale_policy_bundle.go::hashBundle) share a
// single implementation — until then, both copies MUST be kept in
// sync by hand.
func init() {
	AddDBMigrateRecord("20260718001", func(db *gorm.DB) error {
		// AutoMigrate ensures the table exists. If migration
		// 20260713001 was skipped or never ran, this step
		// creates the table.
		if err := db.AutoMigrate(&schema.AutoscalePolicyBundleSpec{}); err != nil {
			return err
		}

		// Build specs via protojson so the JSON format exactly
		// matches what the v2 RPC marshal path produces.
		policies := []*entity.AutoscalePolicySpec{
			// TIDB
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIDB, Direction: apiv1.AutoscalePolicy_SCALE_OUT, Capability: apiv1.AutoscalePolicy_NODE_COUNT, Trigger: apiv1.AutoscalePolicy_CPU, State: apiv1.AutoscalePolicy_ENABLED},
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIDB, Direction: apiv1.AutoscalePolicy_SCALE_IN, Capability: apiv1.AutoscalePolicy_NODE_COUNT, Trigger: apiv1.AutoscalePolicy_CPU, State: apiv1.AutoscalePolicy_ENABLED},
			// per-group TIPROXY
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIPROXY, Direction: apiv1.AutoscalePolicy_SCALE_OUT, Capability: apiv1.AutoscalePolicy_NODE_COUNT, Trigger: apiv1.AutoscalePolicy_CPU, State: apiv1.AutoscalePolicy_ENABLED},
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIPROXY, Direction: apiv1.AutoscalePolicy_SCALE_IN, Capability: apiv1.AutoscalePolicy_NODE_COUNT, Trigger: apiv1.AutoscalePolicy_CPU, State: apiv1.AutoscalePolicy_DISABLED},
			// 3 workers (pool-scope, per-pool HPA)
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIDB_WORKER, Direction: apiv1.AutoscalePolicy_SCALE_OUT, Capability: apiv1.AutoscalePolicy_NODE_COUNT, Trigger: apiv1.AutoscalePolicy_CPU, State: apiv1.AutoscalePolicy_ENABLED},
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIDB_WORKER, Direction: apiv1.AutoscalePolicy_SCALE_IN, Capability: apiv1.AutoscalePolicy_NODE_COUNT, Trigger: apiv1.AutoscalePolicy_CPU, State: apiv1.AutoscalePolicy_ENABLED},
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIKV_WORKER, Direction: apiv1.AutoscalePolicy_SCALE_OUT, Capability: apiv1.AutoscalePolicy_NODE_COUNT, Trigger: apiv1.AutoscalePolicy_CPU, State: apiv1.AutoscalePolicy_ENABLED},
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIKV_WORKER, Direction: apiv1.AutoscalePolicy_SCALE_IN, Capability: apiv1.AutoscalePolicy_NODE_COUNT, Trigger: apiv1.AutoscalePolicy_CPU, State: apiv1.AutoscalePolicy_ENABLED},
			{TargetType: apiv1.AutoscaleTargetType_TARGET_COPROCESSOR_WORKER, Direction: apiv1.AutoscalePolicy_SCALE_OUT, Capability: apiv1.AutoscalePolicy_NODE_COUNT, Trigger: apiv1.AutoscalePolicy_CPU, State: apiv1.AutoscalePolicy_ENABLED},
			{TargetType: apiv1.AutoscaleTargetType_TARGET_COPROCESSOR_WORKER, Direction: apiv1.AutoscalePolicy_SCALE_IN, Capability: apiv1.AutoscalePolicy_NODE_COUNT, Trigger: apiv1.AutoscalePolicy_CPU, State: apiv1.AutoscalePolicy_ENABLED},
			// TIKV × 4 axes (ru_configs.FeatureFlags-backed, not HPA)
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIKV, Direction: apiv1.AutoscalePolicy_SCALE_OUT, Capability: apiv1.AutoscalePolicy_NODE_COUNT, Trigger: apiv1.AutoscalePolicy_CPU, State: apiv1.AutoscalePolicy_ENABLED},
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIKV, Direction: apiv1.AutoscalePolicy_SCALE_OUT, Capability: apiv1.AutoscalePolicy_DISK_SIZE, Trigger: apiv1.AutoscalePolicy_DISK, State: apiv1.AutoscalePolicy_ENABLED},
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIKV, Direction: apiv1.AutoscalePolicy_SCALE_OUT, Capability: apiv1.AutoscalePolicy_IOPS, Trigger: apiv1.AutoscalePolicy_IO, State: apiv1.AutoscalePolicy_ENABLED},
			{TargetType: apiv1.AutoscaleTargetType_TARGET_TIKV, Direction: apiv1.AutoscalePolicy_SCALE_OUT, Capability: apiv1.AutoscalePolicy_THROUGHPUT, Trigger: apiv1.AutoscalePolicy_IO, State: apiv1.AutoscalePolicy_ENABLED},
			// SHARED_POOL / TIDB_GROUP × SCALE_OUT/SCALE_IN × NODE_SPEC/RCU (NEW in 03)
			{TargetType: apiv1.AutoscaleTargetType_SHARED_POOL, Direction: apiv1.AutoscalePolicy_SCALE_OUT, Capability: apiv1.AutoscalePolicy_NODE_SPEC, Trigger: apiv1.AutoscalePolicy_RCU, State: apiv1.AutoscalePolicy_DISABLED},
			{TargetType: apiv1.AutoscaleTargetType_SHARED_POOL, Direction: apiv1.AutoscalePolicy_SCALE_IN, Capability: apiv1.AutoscalePolicy_NODE_SPEC, Trigger: apiv1.AutoscalePolicy_RCU, State: apiv1.AutoscalePolicy_DISABLED},
			{TargetType: apiv1.AutoscaleTargetType_TIDB_GROUP, Direction: apiv1.AutoscalePolicy_SCALE_OUT, Capability: apiv1.AutoscalePolicy_NODE_SPEC, Trigger: apiv1.AutoscalePolicy_RCU, State: apiv1.AutoscalePolicy_DISABLED},
			{TargetType: apiv1.AutoscaleTargetType_TIDB_GROUP, Direction: apiv1.AutoscalePolicy_SCALE_IN, Capability: apiv1.AutoscalePolicy_NODE_SPEC, Trigger: apiv1.AutoscalePolicy_RCU, State: apiv1.AutoscalePolicy_DISABLED},
		}
		parts := make([]string, 0, len(policies))
		for _, spec := range policies {
			raw, err := protojson.Marshal(spec)
			if err != nil {
				return fmt.Errorf("marshal DEFAULT spec: %w", err)
			}
			parts = append(parts, string(raw))
		}
		specsJSON := "[" + strings.Join(parts, ",") + "]"

		bundleHash := hashSeedSpecs18(policies)

		// ON DUPLICATE KEY UPDATE rewrites specs_json +
		// bundle_hash (not just updated_at) so that re-running
		// the migration against a row inserted by 20260713002
		// heals the data in place. The first INSERT supplies
		// all columns because the table does not use gorm.Model;
		// created_at / updated_at / bundle_hash have no GORM
		// magic.
		return db.Exec(`
			INSERT INTO autoscale_policy_bundle_specs
				(scope, subject_id, specs_json, bundle_hash, created_at, updated_at)
			VALUES ('default', '__default__', ?, ?, NOW(), NOW())
			ON DUPLICATE KEY UPDATE
				specs_json = VALUES(specs_json),
				bundle_hash = VALUES(bundle_hash),
				updated_at = NOW()
		`, specsJSON, bundleHash).Error
	})
}

// hashSeedSpecs18 computes the same canonical bundle_hash that
// 20260713002's hashSeedSpecs computed and that the runtime SYNC
// KEYS_ONLY path computes in
// domain/server/autoscale_policy_bundle.go::hashBundle.
//
// TODO(PR-A follow-up): extract this function to
// pkg/autoscale/bundlehash so the migration and runtime share a
// single implementation. Until then, both copies MUST be kept in
// sync by hand — any change to hashBundle MUST be reflected here.
//
// Algorithm (must match 02 and runtime exactly):
//
//  1. Sort specs by (targetType, direction, capability, trigger).
//     Go map iteration order is randomized, so a request-arrival-
//     order slice would otherwise hash differently each call.
//  2. protojson.Marshal each sorted spec — produces stable text
//     serialization with enum values as string names (matching
//     what the v2 RPC marshal path emits).
//  3. SHA-256 over the concatenated protojson bytes separated by
//     "\n".
//  4. Hex-encode the digest and truncate to 32 chars (fits the
//     column's varchar(64) NOT NULL).
func hashSeedSpecs18(specs []*entity.AutoscalePolicySpec) string {
	sorted := make([]*entity.AutoscalePolicySpec, len(specs))
	copy(sorted, specs)
	sort.Slice(sorted, func(i, j int) bool {
		a, b := sorted[i], sorted[j]
		if a.GetTargetType() != b.GetTargetType() {
			return a.GetTargetType() < b.GetTargetType()
		}
		if a.GetDirection() != b.GetDirection() {
			return a.GetDirection() < b.GetDirection()
		}
		if a.GetCapability() != b.GetCapability() {
			return a.GetCapability() < b.GetCapability()
		}
		return a.GetTrigger() < b.GetTrigger()
	})
	h := sha256.New()
	for _, s := range sorted {
		// protojson → stable text serialization independent of map order.
		b, _ := protojson.Marshal(s)
		h.Write(b)
		h.Write([]byte("\n"))
	}
	return hex.EncodeToString(h.Sum(nil))[:32]
}
