package upgrade

// Configuration file upgrade implementation.
// The strategy is to transform only those entries for which rule exists.

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/BurntSushi/toml"
	"go.uber.org/zap"
	"golang.org/x/text/encoding/unicode"
	"golang.org/x/text/transform"
)

// passthroughConfigRules maps v1 config key-names to corresponding v2 config key-names.
// The values of these configs will not be modified during the upgrade process.
var passthroughConfigRules = map[string]string{
	"reporting-disabled":                                   "reporting-disabled",
	"data.dir":                                             "engine-path",
	"data.wal-fsync-delay":                                 "storage-wal-fsync-delay",
	"data.validate-keys":                                   "storage-validate-keys",
	"data.cache-max-memory-size":                           "storage-cache-max-memory-size",
	"data.cache-snapshot-memory-size":                      "storage-cache-snapshot-memory-size",
	"data.cache-snapshot-write-cold-duration":              "storage-cache-snapshot-write-cold-duration",
	"data.compact-full-write-cold-duration":                "storage-compact-full-write-cold-duration",
	"data.compact-throughput-burst":                        "storage-compact-throughput-burst",
	"data.max-concurrent-compactions":                      "storage-max-concurrent-compactions",
	"data.max-index-log-file-size":                         "storage-max-index-log-file-size",
	"data.series-id-set-cache-size":                        "storage-series-id-set-cache-size",
	"data.series-file-max-concurrent-snapshot-compactions": "storage-series-file-max-concurrent-snapshot-compactions",
	"data.tsm-use-madv-willneed":                           "storage-tsm-use-madv-willneed",
	"retention.check-interval":                             "storage-retention-check-interval",
	"shard-precreation.check-interval":                     "storage-shard-precreator-check-interval",
	"shard-precreation.advance-period":                     "storage-shard-precreator-advance-period",
	"coordinator.max-concurrent-queries":                   "query-concurrency",
	"coordinator.max-select-point":                         "influxql-max-select-point",
	"coordinator.max-select-series":                        "influxql-max-select-series",
	"coordinator.max-select-buckets":                       "influxql-max-select-buckets",
	"logging.level":                                        "log-level",
	"http.bind-address":                                    "http-bind-address",
	"http.https-certificate":                               "tls-cert",
	"http.https-private-key":                               "tls-key",
}

func loadV1Config(configFile string) (*configV1, *map[string]interface{}, error) {
	_, err := os.Stat(configFile)
	if err != nil {
		return nil, nil, fmt.Errorf("1.x config file '%s' does not exist", configFile)
	}

	// load 1.x config content into byte array
	bs, err := load(configFile)
	if err != nil {
		return nil, nil, err
	}

	// parse it into simplified v1 config used as return value
	var configV1 configV1
	_, err = toml.Decode(string(bs), &configV1)
	if err != nil {
		return nil, nil, err
	}

	// parse into a generic config map
	var cAny map[string]interface{}
	_, err = toml.Decode(string(bs), &cAny)
	if err != nil {
		return nil, nil, err
	}

	return &configV1, &cAny, nil
}

func load(path string) ([]byte, error) {
	bs, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	// From master-1.x/cmd/influxd/run/config.go:
	// Handle any potential Byte-Order-Marks that may be in the config file.
	// This is for Windows compatibility only.
	// See https://github.com/influxdata/telegraf/issues/1378 and
	// https://github.com/influxdata/influxdb/issues/8965.
	bom := unicode.BOMOverride(transform.Nop)
	bs, _, err = transform.Bytes(bom, bs)

	return bs, err
}

// upgradeConfig upgrades existing 1.x configuration file to 2.x influxdb.toml file.
func upgradeConfig(v1Config map[string]interface{}, targetOptions optionsV2, log *zap.Logger) error {
	// create and initialize helper
	cu := &configUpgrader{
		rules: passthroughConfigRules,
		log:   log,
	}

	// rewrite config options from V1 to V2 paths
	cTransformed := cu.transform(v1Config)

	// update new config with upgrade command options
	cu.updateV2Config(cTransformed, targetOptions)

	// write the ugpraded config to disk
	return cu.save(cTransformed, targetOptions.configPath)
}

// configUpgrader is a helper used by `upgrade-config` command.
type configUpgrader struct {
	rules map[string]string
	log   *zap.Logger
}

func (cu *configUpgrader) updateV2Config(config map[string]interface{}, targetOptions optionsV2) {
	if targetOptions.enginePath != "" {
		config["engine-path"] = targetOptions.enginePath
	}
	if targetOptions.boltPath != "" {
		config["bolt-path"] = targetOptions.boltPath
	}
}

func (cu *configUpgrader) save(config map[string]interface{}, path string) error {
	// Open the target file, creating parent directories if needed.
	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
		return err
	}
	outFile, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer outFile.Close()

	// Encode the config directly into the file as TOML.
	return toml.NewEncoder(outFile).Encode(&config)
}

// Credits: @rogpeppe (Roger Peppe)

func (cu *configUpgrader) transform(v1Config map[string]interface{}) map[string]interface{} {
	res := make(map[string]interface{})
	for old, new := range cu.rules {
		if val, ok := cu.lookup(v1Config, old); ok {
			res[new] = val
		}
	}

	// Special case: flip the value for pprof.
	if val, ok := cu.lookup(v1Config, "http.pprof-enabled"); ok {
		if b, ok := val.(bool); ok {
			res["pprof-disabled"] = !b
		}
	}

	// Special case: ensure query settings are valid.
	fixQueryLimits(res)

	return res
}

// fixQueryLimits ensures that all query-related config settings are compatible
// with the upgraded value of the 'query-concurrency' setting.
func fixQueryLimits(v2Config map[string]interface{}) {
	concurrencyVal, ok := v2Config["query-concurrency"]
	if !ok {
		return
	}
	var concurrency int64
	switch c := concurrencyVal.(type) {
	case int:
		concurrency = int64(c)
	case int32:
		concurrency = int64(c)
	case int64:
		concurrency = c
	default:
		concurrency = 0
	}
	if concurrency == 0 {
		// The upgrade process doesn't generate a value for query-queue-size, so if
		// query-concurrency is 0 / unset then it's safe to leave query-queue-size unset.
		return
	}

	// When query-concurrency is > 0, query-queue-size must also be > 0.
	v2Config["query-queue-size"] = concurrency
}

func (cu *configUpgrader) lookup(v1Config map[string]interface{}, path string) (interface{}, bool) {
	for {
		elem, rest, _ := strings.Cut(path, ".")
		val, ok := v1Config[elem]
		if rest == "" {
			return val, ok
		}
		child, ok := val.(map[string]interface{})
		if !ok {
			return nil, false
		}
		path, v1Config = rest, child
	}
}
