// Package templategen is a small engine that turns a fixed set of Specs into
// configuration templates for a caller-supplied version. Each product/cluster
// family lives in its own subpackage that declares its Specs and wires them with
// New (see the premium subpackage); the engine here is data-agnostic and holds
// no version state — the version is always an input.
//
// A Spec's config is a map of dotted key -> ParamItem. Author it whichever way
// reads best:
//
//	Base: templategen.MustTOML(`<toml>`)   // big structured configs
//	Base: templategen.MustJSON(`<json>`)   // configs authored as JSON (e.g. SEM)
//	Base: map[string]*pb.ParamItem{        // explicit, with per-key comments
//		"native-br.backup-delay": templategen.MustParam("5s", "pre-backup delay"),
//	}
//
// Most params are identical across versions, so a Spec is Base plus an optional
// Apply for per-version deltas. By convention Base holds the LATEST version (the
// source of truth, actively developed) and Apply adapts OLDER / in-test versions
// DOWN from it — so retiring an old version is deleting its Apply branch, never
// editing Base:
//
//	Apply: func(version string, data map[string]*pb.ParamItem) {
//		// older lines diff down from Base; tolerate an optional leading "v"
//		if !strings.HasPrefix(strings.TrimPrefix(version, "v"), "26.3") {
//			data["server.grpc-concurrency"] = templategen.MustParam(5)
//			delete(data, "server.snap-io-max-bytes-per-sec")
//		}
//	}
//
// The engine holds no gRPC client so it can be reused outside ctl (e.g. the
// web/control-plane); callers generate one config type via Generate or all of
// them via GenerateAll and persist the result. A Spec with no Base yields an
// empty template (no params).
package templategen

import (
	"bytes"
	"encoding/json"
	"fmt"
	"maps"

	"github.com/BurntSushi/toml"
	"github.com/tidbcloud/config-center/pkg/utils"
	pb "github.com/tidbcloud/config-center/proto/config_center"
)

// Binding describes one default-template binding a generated template should be
// installed as via UpdateDefaultConfigurationTemplate. All bindings of a
// template share the template's Version.
type Binding struct {
	ClusterType pb.ClusterType
	ConfigType  string
}

// Template is a fully-resolved configuration template for one config type at the
// requested version: the data to create plus the default bindings it should drive.
type Template struct {
	// CreateClusterType is the cluster type the template is created under
	// (always SHARED_POOL for the premium generator today).
	CreateClusterType pb.ClusterType
	// ConfigType is the configuration type used when creating the template.
	// Note it may differ from a binding's ConfigType (e.g. dumpling is created
	// under the SHARED_POOL "dumpling" config type but bound for LOGICAL_CLUSTER).
	ConfigType string
	// Name is the resolved, version-suffixed template name.
	Name string
	// Version is the version this template was generated for: the key used for
	// its default-template bindings (and the image tag / config lookup downstream).
	Version string
	// Data is the flattened param map ready for CreateConfigurationTemplate. It is
	// non-nil but may be empty (a template with no params).
	Data map[string]*pb.ParamItem
	// Bindings are the default-template bindings to install for this template,
	// each at Version.
	Bindings []Binding
}

// Spec declares one config type's template: a Base config map plus an optional
// Apply func for per-version deltas. Bindings is optional: when nil, the template
// is bound as the SHARED_POOL default for its own config type. Only templates
// shared across cluster types set Bindings explicitly.
type Spec struct {
	ConfigType string
	Name       string
	// Base holds the LATEST version's config and is the source of truth. Build it
	// with MustTOML / MustJSON, or write it directly with MustParam for per-key
	// comments. A nil Base yields an empty template (no params).
	Base map[string]*pb.ParamItem
	// Apply adapts OLDER / in-test versions DOWN from Base: gate on the version
	// and set data[key]=MustParam(...) / delete(data, key) only for the keys an
	// older line needs to differ. nil means every version uses Base unchanged.
	Apply    func(version string, data map[string]*pb.ParamItem)
	Bindings []Binding
}

// Generator turns a fixed set of Specs into templates. Each product/cluster
// family constructs its own Generator via New; the engine never references a
// global registry.
type Generator struct {
	specs []Spec
}

// New builds a Generator from specs, preserving the given order.
func New(specs []Spec) *Generator {
	return &Generator{specs: specs}
}

// ConfigTypes returns the config types this generator can produce, in a stable
// order. ctl iterates this to drive the full base-data migration; web can use
// it to enumerate the configurable components.
func (g *Generator) ConfigTypes() []string {
	out := make([]string, len(g.specs))
	for i, s := range g.specs {
		out[i] = s.ConfigType
	}
	return out
}

// Generate returns the template for a single config type at the given version.
// This per-config-type entry point is the one intended for web integration.
func (g *Generator) Generate(configType, version string) (Template, error) {
	for _, s := range g.specs {
		if s.ConfigType == configType {
			return build(s, version)
		}
	}
	return Template{}, fmt.Errorf("templategen: unknown config type %q", configType)
}

// GenerateAll returns the templates for every config type, in ConfigTypes order.
// versionFor resolves the version for each config type, so components that follow
// the cluster version and components with an independent release line can coexist.
func (g *Generator) GenerateAll(versionFor func(configType string) string) ([]Template, error) {
	out := make([]Template, 0, len(g.specs))
	for _, s := range g.specs {
		t, err := build(s, versionFor(s.ConfigType))
		if err != nil {
			return nil, err
		}
		out = append(out, t)
	}
	return out, nil
}

// build resolves a Spec into a ready-to-create Template at the given version.
func build(s Spec, version string) (Template, error) {
	if version == "" {
		return Template{}, fmt.Errorf("templategen: empty version for %q", s.ConfigType)
	}
	bindings := s.Bindings
	if bindings == nil {
		// Default: bind as the SHARED_POOL default for this config type.
		bindings = []Binding{{ClusterType: pb.ClusterType_SHARED_POOL, ConfigType: s.ConfigType}}
	}
	// Shallow-clone so Apply (which replaces/deletes whole entries) never mutates
	// the shared Base across versions or calls. A nil Base yields an empty template.
	data := maps.Clone(s.Base)
	if data == nil {
		data = map[string]*pb.ParamItem{}
	}
	if s.Apply != nil {
		s.Apply(version, data)
	}
	return Template{
		CreateClusterType: pb.ClusterType_SHARED_POOL,
		ConfigType:        s.ConfigType,
		Name:              s.Name + " " + version,
		Version:           version,
		Data:              data,
		Bindings:          bindings,
	}, nil
}

// MustParam builds a ParamItem from a Go value (string/number/bool/...) and an
// optional comment. It panics on an unrepresentable value (Must* because specs
// are static data, so authoring mistakes surface at init/test time).
func MustParam(v any, comment ...string) *pb.ParamItem {
	val, err := pb.NewValue(v)
	if err != nil {
		panic(fmt.Sprintf("templategen.MustParam: %v", err))
	}
	c := ""
	if len(comment) > 0 {
		c = comment[0]
	}
	return &pb.ParamItem{Value: val, Comment: c}
}

// MustTOML parses a TOML document into a flattened param map. It panics on
// malformed input (specs are static data, so errors show up at init/test time).
func MustTOML(tomlConfig string) map[string]*pb.ParamItem {
	var cfg map[string]any
	if _, err := toml.NewDecoder(bytes.NewReader([]byte(tomlConfig))).Decode(&cfg); err != nil {
		panic(fmt.Sprintf("templategen.MustTOML: %v", err))
	}
	return paramsFromFlat(utils.FlattenMap(cfg))
}

// MustJSON parses a JSON document into a flattened param map. It panics on
// malformed input (specs are static data, so errors show up at init/test time).
func MustJSON(jsonConfig string) map[string]*pb.ParamItem {
	var cfg map[string]any
	if err := json.Unmarshal([]byte(jsonConfig), &cfg); err != nil {
		panic(fmt.Sprintf("templategen.MustJSON: %v", err))
	}
	return paramsFromFlat(utils.FlattenMap(cfg))
}

func paramsFromFlat(flat map[string]any) map[string]*pb.ParamItem {
	out := make(map[string]*pb.ParamItem, len(flat))
	for k, v := range flat {
		out[k] = MustParam(v)
	}
	return out
}
