package config

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

	"github.com/influxdata/toml/ast"
)

type keyValuePair struct {
	Key   string
	Value string
}

func processTable(parent string, table *ast.Table) ([]keyValuePair, error) {
	var prefix string
	var options []keyValuePair

	if parent != "" {
		prefix = parent + "."
	}

	for k, value := range table.Fields {
		switch v := value.(type) {
		case *ast.KeyValue:
			key := prefix + k
			options = append(options, keyValuePair{
				Key:   key,
				Value: v.Value.Source(),
			})
		case *ast.Table:
			key := prefix + k
			childs, err := processTable(key, v)
			if err != nil {
				return nil, fmt.Errorf("parsing table for %q failed: %w", key, err)
			}
			options = append(options, childs...)
		case []*ast.Table:
			for i, t := range v {
				key := fmt.Sprintf("%s#%d.%s", prefix, i, k)
				childs, err := processTable(key, t)
				if err != nil {
					return nil, fmt.Errorf("parsing table for %q #%d failed: %w", key, i, err)
				}
				options = append(options, childs...)
			}
		default:
			return nil, fmt.Errorf("unknown node type %T in key %q", value, prefix+k)
		}
	}
	return options, nil
}

func generatePluginID(prefix string, table *ast.Table) (string, error) {
	// We need to ensure that identically configured plugins _always_
	// result in the same ID no matter which order the options are specified.
	// This is even more relevant as Golang does _not_ give any guarantee
	// on the ordering of maps.
	// So we flatten out the configuration options (also for nested objects)
	// and then sort the resulting array by the canonical key-name.
	cfg, err := processTable("", table)
	if err != nil {
		return "", fmt.Errorf("processing AST failed: %w", err)
	}
	sort.SliceStable(cfg, func(i, j int) bool { return cfg[i].Key < cfg[j].Key })

	// Hash the config options to get the ID. We also prefix the ID with
	// the plugin name to prevent overlap with other plugin types.
	hash := sha256.New()
	if _, err := hash.Write(append([]byte(prefix), 0)); err != nil {
		return "", fmt.Errorf("hashing name failed: %w", err)
	}
	for _, kv := range cfg {
		if _, err := hash.Write([]byte(kv.Key + ":" + kv.Value)); err != nil {
			return "", fmt.Errorf("hashing entry %q failed: %w", kv.Key, err)
		}
		if _, err := hash.Write([]byte{0}); err != nil {
			return "", fmt.Errorf("adding option end marker failed: %w", err)
		}
	}

	return hex.EncodeToString(hash.Sum(nil)), nil
}
