// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0

package cli

import (
	"io"
	"net/url"
	"os"
	"reflect"
	"strconv"
	"strings"

	"github.com/absmach/magistrala/pkg/errors"
	smqsdk "github.com/absmach/magistrala/pkg/sdk"
	"github.com/pelletier/go-toml"
	"github.com/spf13/cobra"
)

const (
	defURL             string = "http://localhost"
	defUsersURL        string = defURL + ":9002"
	defCLientsURL      string = defURL + ":9006"
	defDomainsURL      string = defURL + ":9003"
	defChannelsURL     string = defURL + ":9005"
	defGroupsURL       string = defURL + ":9004"
	defHTTPURL         string = defURL + ":8008"
	defTLSVerification bool   = false
	defOffset          string = "0"
	defLimit           string = "10"
	defTopic           string = ""
	defRawOutput       string = "false"
)

type remotes struct {
	ClientsURL      string `toml:"clients_url"`
	UsersURL        string `toml:"users_url"`
	DomainsURL      string `toml:"domains_url"`
	ChannelsURL     string `toml:"channels_url"`
	GroupsURL       string `toml:"groups_url"`
	HTTPAdapterURL  string `toml:"http_adapter_url"`
	CertsURL        string `toml:"certs_url"`
	HostURL         string `toml:"host_url"`
	TLSVerification bool   `toml:"tls_verification"`
}

type filter struct {
	Offset string `toml:"offset"`
	Limit  string `toml:"limit"`
	Topic  string `toml:"topic"`
}

type config struct {
	Remotes   remotes `toml:"remotes"`
	Filter    filter  `toml:"filter"`
	UserToken string  `toml:"user_token"`
	RawOutput string  `toml:"raw_output"`
}

// Readable by all user groups but writeable by the user only.
const filePermission = 0o644

var (
	errReadFail            = errors.New("failed to read config file")
	errNoKey               = errors.New("no such key")
	errUnsupportedKeyValue = errors.New("unsupported data type for key")
	errWritingConfig       = errors.New("error in writing the updated config to file")
	errInvalidURL          = errors.New("invalid url")
	errURLParseFail        = errors.New("failed to parse url")
	defaultConfigPath      = "./config.toml"
)

func read(file string) (config, error) {
	c := config{}
	data, err := os.Open(file)
	if err != nil {
		return c, errors.Wrap(errReadFail, err)
	}
	defer data.Close()

	buf, err := io.ReadAll(data)
	if err != nil {
		return c, errors.Wrap(errReadFail, err)
	}

	if err := toml.Unmarshal(buf, &c); err != nil {
		return config{}, err
	}

	return c, nil
}

// ParseConfig - parses the config file.
func ParseConfig(sdkConf smqsdk.Config) (smqsdk.Config, error) {
	if ConfigPath == "" {
		ConfigPath = defaultConfigPath
	}

	_, err := os.Stat(ConfigPath)
	switch {
	// If the file does not exist, create it with default values.
	case os.IsNotExist(err):
		defaultConfig := config{
			Remotes: remotes{
				ClientsURL:      defCLientsURL,
				UsersURL:        defUsersURL,
				DomainsURL:      defDomainsURL,
				ChannelsURL:     defChannelsURL,
				GroupsURL:       defGroupsURL,
				HTTPAdapterURL:  defHTTPURL,
				HostURL:         defURL,
				TLSVerification: defTLSVerification,
			},
			Filter: filter{
				Offset: defOffset,
				Limit:  defLimit,
				Topic:  defTopic,
			},
			RawOutput: defRawOutput,
		}
		buf, err := toml.Marshal(defaultConfig)
		if err != nil {
			return sdkConf, err
		}
		if err = os.WriteFile(ConfigPath, buf, filePermission); err != nil {
			return sdkConf, errors.Wrap(errWritingConfig, err)
		}
	case err != nil:
		return sdkConf, err
	}

	config, err := read(ConfigPath)
	if err != nil {
		return sdkConf, err
	}

	if config.Filter.Offset != "" && Offset == 0 {
		offset, err := strconv.ParseUint(config.Filter.Offset, 10, 64)
		if err != nil {
			return sdkConf, err
		}
		Offset = offset
	}

	if config.Filter.Limit != "" && Limit == 0 {
		limit, err := strconv.ParseUint(config.Filter.Limit, 10, 64)
		if err != nil {
			return sdkConf, err
		}
		Limit = limit
	}

	if config.Filter.Topic != "" && Topic == "" {
		Topic = config.Filter.Topic
	}

	if config.RawOutput != "" {
		rawOutput, err := strconv.ParseBool(config.RawOutput)
		if err != nil {
			return sdkConf, err
		}
		// check for config file value or flag input value is true
		RawOutput = rawOutput || RawOutput
	}

	if sdkConf.ClientsURL == "" && config.Remotes.ClientsURL != "" {
		sdkConf.ClientsURL = config.Remotes.ClientsURL
	}

	if sdkConf.UsersURL == "" && config.Remotes.UsersURL != "" {
		sdkConf.UsersURL = config.Remotes.UsersURL
	}

	if sdkConf.DomainsURL == "" && config.Remotes.DomainsURL != "" {
		sdkConf.DomainsURL = config.Remotes.DomainsURL
	}

	if sdkConf.ChannelsURL == "" && config.Remotes.ChannelsURL != "" {
		sdkConf.ChannelsURL = config.Remotes.ChannelsURL
	}

	if sdkConf.GroupsURL == "" && config.Remotes.GroupsURL != "" {
		sdkConf.GroupsURL = config.Remotes.GroupsURL
	}

	if sdkConf.HTTPAdapterURL == "" && config.Remotes.HTTPAdapterURL != "" {
		sdkConf.HTTPAdapterURL = config.Remotes.HTTPAdapterURL
	}

	if sdkConf.CertsURL == "" && config.Remotes.CertsURL != "" {
		sdkConf.CertsURL = config.Remotes.CertsURL
	}

	if sdkConf.HostURL == "" && config.Remotes.HostURL != "" {
		sdkConf.HostURL = config.Remotes.HostURL
	}

	sdkConf.TLSVerification = config.Remotes.TLSVerification || sdkConf.TLSVerification

	return sdkConf, nil
}

// New config command to store params to local TOML file.
func NewConfigCmd() *cobra.Command {
	return &cobra.Command{
		Use:   "config <key> <value>",
		Short: "CLI local config",
		Long:  "Local param storage to prevent repetitive passing of keys",
		Run: func(cmd *cobra.Command, args []string) {
			if len(args) != 2 {
				logUsageCmd(*cmd, cmd.Use)
				return
			}

			if err := setConfigValue(args[0], args[1]); err != nil {
				logErrorCmd(*cmd, err)
				return
			}

			logOKCmd(*cmd)
		},
	}
}

func setConfigValue(key, value string) error {
	config, err := read(ConfigPath)
	if err != nil {
		return err
	}

	if strings.Contains(key, "url") {
		u, err := url.Parse(value)
		if err != nil {
			return errors.Wrap(errInvalidURL, err)
		}
		if u.Scheme == "" || u.Host == "" {
			return errors.Wrap(errInvalidURL, err)
		}
		if u.Scheme != "http" && u.Scheme != "https" {
			return errors.Wrap(errURLParseFail, err)
		}
	}

	configKeyToField := map[string]any{
		"clients_url":      &config.Remotes.ClientsURL,
		"users_url":        &config.Remotes.UsersURL,
		"http_adapter_url": &config.Remotes.HTTPAdapterURL,
		"certs_url":        &config.Remotes.CertsURL,
		"tls_verification": &config.Remotes.TLSVerification,
		"offset":           &config.Filter.Offset,
		"limit":            &config.Filter.Limit,
		"topic":            &config.Filter.Topic,
		"raw_output":       &config.RawOutput,
		"user_token":       &config.UserToken,
	}

	fieldPtr, ok := configKeyToField[key]
	if !ok {
		return errNoKey
	}

	fieldValue := reflect.ValueOf(fieldPtr).Elem()

	switch fieldValue.Kind() {
	case reflect.String:
		fieldValue.SetString(value)
	case reflect.Int:
		intValue, err := strconv.Atoi(value)
		if err != nil {
			return err
		}
		fieldValue.SetUint(uint64(intValue))
	case reflect.Bool:
		boolValue, err := strconv.ParseBool(value)
		if err != nil {
			return err
		}
		fieldValue.SetBool(boolValue)
	default:
		return errors.Wrap(errUnsupportedKeyValue, err)
	}

	buf, err := toml.Marshal(config)
	if err != nil {
		return err
	}

	if err = os.WriteFile(ConfigPath, buf, filePermission); err != nil {
		return errors.Wrap(errWritingConfig, err)
	}

	return nil
}
