// package config implements the ipfs config file datastructures and utilities.
package config

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"strings"

	"github.com/ipfs/kubo/misc/fsutil"
)

// Config is used to load ipfs config files.
type Config struct {
	Identity  Identity  // local node's peer identity
	Datastore Datastore // local node's storage
	Addresses Addresses // local node's addresses
	Mounts    Mounts    // local node's mount points
	Discovery Discovery // local node's discovery mechanisms
	Routing   Routing   // local node's routing settings
	Ipns      Ipns      // Ipns settings
	Bootstrap []string  // local nodes's bootstrap peer addresses
	Gateway   Gateway   // local node's gateway server options
	API       API       // local node's API settings
	Swarm     SwarmConfig
	AutoNAT   AutoNATConfig
	AutoTLS   AutoTLS
	Pubsub    PubsubConfig
	Peering   Peering
	DNS       DNS

	Migration Migration
	AutoConf  AutoConf

	Provide       Provide    // Merged Provider and Reprovider configuration
	Provider      Provider   // Deprecated: use Provide. Will be removed in a future release.
	Reprovider    Reprovider // Deprecated: use Provide. Will be removed in a future release.
	HTTPRetrieval HTTPRetrieval
	Experimental  Experiments
	Plugins       Plugins
	Pinning       Pinning
	Import        Import
	Version       Version

	Internal Internal // experimental/unstable options

	Bitswap Bitswap `json:",omitempty"`
}

const (
	// DefaultPathName is the default config dir name.
	DefaultPathName = ".ipfs"
	// DefaultPathRoot is the path to the default config dir location.
	DefaultPathRoot = "~/" + DefaultPathName
	// DefaultConfigFile is the filename of the configuration file.
	DefaultConfigFile = "config"
	// EnvDir is the environment variable used to change the path root.
	EnvDir = "IPFS_PATH"
)

// PathRoot returns the default configuration root directory.
func PathRoot() (string, error) {
	dir := os.Getenv(EnvDir)
	var err error
	if len(dir) == 0 {
		dir, err = fsutil.ExpandHome(DefaultPathRoot)
	}
	return dir, err
}

// Path returns the path `extension` relative to the configuration root. If an
// empty string is provided for `configroot`, the default root is used.
func Path(configroot, extension string) (string, error) {
	if len(configroot) == 0 {
		dir, err := PathRoot()
		if err != nil {
			return "", err
		}
		return filepath.Join(dir, extension), nil

	}
	return filepath.Join(configroot, extension), nil
}

// Filename returns the configuration file path given a configuration root
// directory and a user-provided configuration file path argument with the
// following rules:
//   - If the user-provided configuration file path is empty, use the default one.
//   - If the configuration root directory is empty, use the default one.
//   - If the user-provided configuration file path is only a file name, use the
//     configuration root directory, otherwise use only the user-provided path
//     and ignore the configuration root.
func Filename(configroot, userConfigFile string) (string, error) {
	if userConfigFile == "" {
		return Path(configroot, DefaultConfigFile)
	}

	if filepath.Dir(userConfigFile) == "." {
		return Path(configroot, userConfigFile)
	}

	return userConfigFile, nil
}

// HumanOutput gets a config value ready for printing.
func HumanOutput(value interface{}) ([]byte, error) {
	s, ok := value.(string)
	if ok {
		return []byte(strings.Trim(s, "\n")), nil
	}
	return Marshal(value)
}

// Marshal configuration with JSON.
func Marshal(value interface{}) ([]byte, error) {
	// need to prettyprint, hence MarshalIndent, instead of Encoder
	return json.MarshalIndent(value, "", "  ")
}

func FromMap(v map[string]interface{}) (*Config, error) {
	buf := new(bytes.Buffer)
	if err := json.NewEncoder(buf).Encode(v); err != nil {
		return nil, err
	}
	var conf Config
	if err := json.NewDecoder(buf).Decode(&conf); err != nil {
		return nil, fmt.Errorf("failure to decode config: %w", err)
	}
	return &conf, nil
}

func ToMap(conf *Config) (map[string]interface{}, error) {
	buf := new(bytes.Buffer)
	if err := json.NewEncoder(buf).Encode(conf); err != nil {
		return nil, err
	}
	var m map[string]interface{}
	if err := json.NewDecoder(buf).Decode(&m); err != nil {
		return nil, fmt.Errorf("failure to decode config: %w", err)
	}
	return m, nil
}

// Convert config to a map, without using encoding/json, since
// zero/empty/'omitempty' fields are excluded by encoding/json during
// marshaling.
func ReflectToMap(conf interface{}) interface{} {
	v := reflect.ValueOf(conf)
	if !v.IsValid() {
		return nil
	}

	// Handle pointer type
	if v.Kind() == reflect.Ptr {
		if v.IsNil() {
			// Create a zero value of the pointer's element type
			elemType := v.Type().Elem()
			zero := reflect.Zero(elemType)
			return ReflectToMap(zero.Interface())
		}
		v = v.Elem()
	}

	switch v.Kind() {
	case reflect.Struct:
		result := make(map[string]interface{})
		t := v.Type()
		for i := 0; i < v.NumField(); i++ {
			field := v.Field(i)
			// Only include exported fields
			if field.CanInterface() {
				result[t.Field(i).Name] = ReflectToMap(field.Interface())
			}
		}
		return result

	case reflect.Map:
		result := make(map[string]interface{})
		iter := v.MapRange()
		for iter.Next() {
			key := iter.Key()
			// Convert map keys to strings for consistency
			keyStr := fmt.Sprint(ReflectToMap(key.Interface()))
			result[keyStr] = ReflectToMap(iter.Value().Interface())
		}
		// Add a sample to differentiate between a map and a struct on validation.
		sample := reflect.Zero(v.Type().Elem())
		if sample.CanInterface() {
			result["*"] = ReflectToMap(sample.Interface())
		}
		return result

	case reflect.Slice, reflect.Array:
		result := make([]interface{}, v.Len())
		for i := 0; i < v.Len(); i++ {
			result[i] = ReflectToMap(v.Index(i).Interface())
		}
		return result

	default:
		// For basic types (int, string, etc.), just return the value
		if v.CanInterface() {
			return v.Interface()
		}
		return nil
	}
}

// Clone copies the config. Use when updating.
func (c *Config) Clone() (*Config, error) {
	var newConfig Config
	var buf bytes.Buffer

	if err := json.NewEncoder(&buf).Encode(c); err != nil {
		return nil, fmt.Errorf("failure to encode config: %w", err)
	}

	if err := json.NewDecoder(&buf).Decode(&newConfig); err != nil {
		return nil, fmt.Errorf("failure to decode config: %w", err)
	}

	return &newConfig, nil
}

// Check if the provided key is present in the structure.
func CheckKey(key string) error {
	conf := Config{}

	// Convert an empty config to a map without JSON.
	cursor := ReflectToMap(&conf)

	// Parse the key and verify it's presence in the map.
	var ok bool
	var mapCursor map[string]interface{}

	parts := strings.Split(key, ".")
	for i, part := range parts {
		mapCursor, ok = cursor.(map[string]interface{})
		if !ok {
			if cursor == nil {
				return nil
			}
			path := strings.Join(parts[:i], ".")
			return fmt.Errorf("%s key is not a map", path)
		}

		cursor, ok = mapCursor[part]
		if !ok {
			// If the config sections is a map, validate against the default entry.
			if cursor, ok = mapCursor["*"]; ok {
				continue
			}
			path := strings.Join(parts[:i+1], ".")
			return fmt.Errorf("%s not found", path)
		}
	}
	return nil
}
