/*
Copyright 2021 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package consul

import (
	"encoding/json"
	"fmt"
	"time"

	consul "github.com/hashicorp/consul/api"

	"github.com/dapr/kit/config"
)

// The intermediateConfig is based off of the consul api types. User configurations are
// deserialized into this type before being converted to the equivalent consul types
// that way breaking changes in future versions of the consul api cannot break user configuration.
type intermediateConfig struct {
	Client               *Config
	Checks               []*AgentServiceCheck
	Tags                 []string
	Meta                 map[string]string
	QueryOptions         *QueryOptions
	AdvancedRegistration *AgentServiceRegistration // advanced use-case
	SelfRegister         bool
	DaprPortMetaKey      string
}

type configSpec struct {
	Client               *consul.Config
	Checks               []*consul.AgentServiceCheck
	Tags                 []string
	Meta                 map[string]string
	QueryOptions         *consul.QueryOptions
	AdvancedRegistration *consul.AgentServiceRegistration // advanced use-case
	SelfRegister         bool
	DaprPortMetaKey      string
}

func parseConfig(rawConfig interface{}) (configSpec, error) {
	var result configSpec
	rawConfig, err := config.Normalize(rawConfig)
	if err != nil {
		return result, err
	}

	data, err := json.Marshal(rawConfig)
	if err != nil {
		return result, fmt.Errorf("error serializing to json: %w", err)
	}

	var configuration intermediateConfig
	err = json.Unmarshal(data, &configuration)
	if err != nil {
		return result, fmt.Errorf("error deserializing to configSpec: %w", err)
	}

	result = mapConfig(configuration)

	return result, nil
}

func mapConfig(config intermediateConfig) configSpec {
	return configSpec{
		Client:               mapClientConfig(config.Client),
		Checks:               mapChecks(config.Checks),
		Tags:                 config.Tags,
		Meta:                 config.Meta,
		QueryOptions:         mapQueryOptions(config.QueryOptions),
		AdvancedRegistration: mapAdvancedRegistration(config.AdvancedRegistration),
		SelfRegister:         config.SelfRegister,
		DaprPortMetaKey:      config.DaprPortMetaKey,
	}
}

func mapClientConfig(config *Config) *consul.Config {
	if config == nil {
		return nil
	}

	mapBasicAuth := func(config *HTTPBasicAuth) *consul.HttpBasicAuth {
		if config == nil {
			return nil
		}

		return &consul.HttpBasicAuth{
			Username: config.Username,
			Password: config.Password,
		}
	}

	return &consul.Config{
		Address:    config.Address,
		Scheme:     config.Scheme,
		Datacenter: config.Datacenter,
		HttpAuth:   mapBasicAuth(config.HTTPAuth),
		WaitTime:   config.WaitTime,
		Token:      config.Token,
		TokenFile:  config.TokenFile,
		TLSConfig: consul.TLSConfig{
			Address:            config.TLSConfig.Address,
			CAFile:             config.TLSConfig.CAFile,
			CAPath:             config.TLSConfig.CAPath,
			CertFile:           config.TLSConfig.CertFile,
			KeyFile:            config.TLSConfig.KeyFile,
			InsecureSkipVerify: config.TLSConfig.InsecureSkipVerify,
		},
	}
}

func mapChecks(config []*AgentServiceCheck) []*consul.AgentServiceCheck {
	if config == nil {
		return nil
	}

	mapped := []*consul.AgentServiceCheck{}

	for i := 0; i < len(config); i++ {
		mapped = append(mapped, mapCheck(config[i]))
	}

	return mapped
}

func mapCheck(config *AgentServiceCheck) *consul.AgentServiceCheck {
	if config == nil {
		return nil
	}

	return &consul.AgentServiceCheck{
		CheckID:                        config.CheckID,
		Name:                           config.Name,
		Args:                           config.Args,
		DockerContainerID:              config.DockerContainerID,
		Shell:                          config.Shell,
		Interval:                       config.Interval,
		Timeout:                        config.Timeout,
		TTL:                            config.TTL,
		HTTP:                           config.HTTP,
		Header:                         config.Header,
		Method:                         config.Method,
		TCP:                            config.TCP,
		Status:                         config.Status,
		Notes:                          config.Notes,
		TLSSkipVerify:                  config.TLSSkipVerify,
		GRPC:                           config.GRPC,
		GRPCUseTLS:                     config.GRPCUseTLS,
		AliasNode:                      config.AliasNode,
		AliasService:                   config.AliasService,
		DeregisterCriticalServiceAfter: config.DeregisterCriticalServiceAfter,
	}
}

func mapQueryOptions(config *QueryOptions) *consul.QueryOptions {
	if config == nil {
		return nil
	}

	return &consul.QueryOptions{
		Datacenter:        config.Datacenter,
		AllowStale:        config.AllowStale,
		RequireConsistent: config.RequireConsistent,
		UseCache:          config.UseCache,
		MaxAge:            config.MaxAge,
		StaleIfError:      config.StaleIfError,
		WaitIndex:         config.WaitIndex,
		WaitHash:          config.WaitHash,
		WaitTime:          config.WaitTime,
		Token:             config.Token,
		Near:              config.Near,
		NodeMeta:          config.NodeMeta,
		RelayFactor:       config.RelayFactor,
		LocalOnly:         config.LocalOnly,
		Connect:           config.Connect,
		Filter:            config.Filter,
	}
}

func mapAdvancedRegistration(config *AgentServiceRegistration) *consul.AgentServiceRegistration {
	if config == nil {
		return nil
	}

	mapExposeConfig := func(config ExposeConfig) consul.ExposeConfig {
		mapped := consul.ExposeConfig{}

		mapped.Checks = config.Checks

		for i := 0; i < len(config.Paths); i++ {
			tmp := consul.ExposePath{
				ListenerPort:    config.Paths[i].ListenerPort,
				Path:            config.Paths[i].Path,
				LocalPathPort:   config.Paths[i].LocalPathPort,
				Protocol:        config.Paths[i].Protocol,
				ParsedFromCheck: config.Paths[i].ParsedFromCheck,
			}
			mapped.Paths = append(mapped.Paths, tmp)
		}

		return mapped
	}

	mapUpstreams := func(config []Upstream) []consul.Upstream {
		if config == nil {
			return nil
		}

		mapped := []consul.Upstream{}

		for i := 0; i < len(config); i++ {
			tmp := consul.Upstream{
				DestinationType:      consul.UpstreamDestType(config[i].DestinationType),
				DestinationNamespace: config[i].DestinationNamespace,
				DestinationName:      config[i].DestinationName,
				Datacenter:           config[i].Datacenter,
				LocalBindAddress:     config[i].LocalBindAddress,
				LocalBindPort:        config[i].LocalBindPort,
				Config:               config[i].Config,
				MeshGateway:          consul.MeshGatewayConfig{Mode: consul.MeshGatewayMode(config[i].MeshGateway.Mode)},
			}
			mapped = append(mapped, tmp)
		}

		return mapped
	}

	mapProxy := func(config *AgentServiceConnectProxyConfig) *consul.AgentServiceConnectProxyConfig {
		if config == nil {
			return nil
		}

		return &consul.AgentServiceConnectProxyConfig{
			DestinationServiceName: config.DestinationServiceName,
			DestinationServiceID:   config.DestinationServiceID,
			LocalServiceAddress:    config.LocalServiceAddress,
			LocalServicePort:       config.LocalServicePort,
			Config:                 config.Config,
			Upstreams:              mapUpstreams(config.Upstreams),
			MeshGateway:            consul.MeshGatewayConfig{Mode: consul.MeshGatewayMode(config.MeshGateway.Mode)},
			Expose:                 mapExposeConfig(config.Expose),
		}
	}

	mapAgentServiceChecks := func(config AgentServiceChecks) consul.AgentServiceChecks {
		if config == nil {
			return nil
		}

		mapped := consul.AgentServiceChecks{}

		for i := 0; i < len(config); i++ {
			mapped = append(mapped, mapCheck(config[i]))
		}

		return mapped
	}

	mapTaggedAddresses := func(config map[string]ServiceAddress) map[string]consul.ServiceAddress {
		if config == nil {
			return nil
		}

		mapped := map[string]consul.ServiceAddress{}
		for k, v := range config {
			mapped[k] = consul.ServiceAddress{
				Address: v.Address,
				Port:    v.Port,
			}
		}

		return mapped
	}

	mapConnect := func(config *AgentServiceConnect) *consul.AgentServiceConnect {
		if config == nil {
			return nil
		}

		return &consul.AgentServiceConnect{
			Native:         config.Native,
			SidecarService: mapAdvancedRegistration(config.SidecarService),
		}
	}

	mapAgentWeights := func(config *AgentWeights) *consul.AgentWeights {
		if config == nil {
			return nil
		}

		return &consul.AgentWeights{
			Passing: config.Passing,
			Warning: config.Warning,
		}
	}

	mapped := &consul.AgentServiceRegistration{
		Kind:              consul.ServiceKind(config.Kind),
		ID:                config.ID,
		Name:              config.Name,
		Tags:              config.Tags,
		Port:              config.Port,
		Address:           config.Address,
		TaggedAddresses:   mapTaggedAddresses(config.TaggedAddresses),
		EnableTagOverride: config.EnableTagOverride,
		Meta:              config.Meta,
		Weights:           mapAgentWeights(config.Weights),
		Check:             mapCheck(config.Check),
		Checks:            mapAgentServiceChecks(config.Checks),
		Proxy:             mapProxy(config.Proxy),
		Connect:           mapConnect(config.Connect),
	}

	return mapped
}

type HTTPBasicAuth struct {
	Username string
	Password string
}

type Config struct {
	Address    string
	Scheme     string
	Datacenter string
	HTTPAuth   *HTTPBasicAuth
	WaitTime   time.Duration
	Token      string
	TokenFile  string
	TLSConfig  TLSConfig
}

type TLSConfig struct {
	Address            string
	CAFile             string
	CAPath             string
	CertFile           string
	KeyFile            string
	InsecureSkipVerify bool
}

type AgentServiceCheck struct {
	Args                           []string
	CheckID                        string
	Name                           string
	DockerContainerID              string
	Shell                          string
	Interval                       string
	Timeout                        string
	TTL                            string
	HTTP                           string
	Method                         string
	TCP                            string
	Status                         string
	Notes                          string
	GRPC                           string
	AliasNode                      string
	AliasService                   string
	DeregisterCriticalServiceAfter string
	Header                         map[string][]string
	TLSSkipVerify                  bool
	GRPCUseTLS                     bool
}

type QueryOptions struct {
	Namespace         string
	Partition         string
	Datacenter        string
	WaitHash          string
	Token             string
	Near              string
	Filter            string
	MaxAge            time.Duration
	StaleIfError      time.Duration
	WaitIndex         uint64
	WaitTime          time.Duration
	NodeMeta          map[string]string
	AllowStale        bool
	RequireConsistent bool
	UseCache          bool
	RelayFactor       uint8
	LocalOnly         bool
	Connect           bool
}

type AgentServiceRegistration struct {
	Kind              string // original: type ServiceKind string
	ID                string
	Name              string
	Tags              []string
	Port              int
	Address           string
	TaggedAddresses   map[string]ServiceAddress
	EnableTagOverride bool
	Meta              map[string]string
	Weights           *AgentWeights
	Check             *AgentServiceCheck
	Checks            AgentServiceChecks
	Proxy             *AgentServiceConnectProxyConfig
	Connect           *AgentServiceConnect
}

type AgentServiceChecks []*AgentServiceCheck

type ServiceAddress struct {
	Address string
	Port    int
}

type AgentWeights struct {
	Passing int
	Warning int
}

type AgentServiceConnectProxyConfig struct {
	DestinationServiceName string
	DestinationServiceID   string
	LocalServiceAddress    string
	LocalServicePort       int
	Config                 map[string]interface{}
	Upstreams              []Upstream
	MeshGateway            MeshGatewayConfig
	Expose                 ExposeConfig
}

type AgentServiceConnect struct {
	Native         bool
	SidecarService *AgentServiceRegistration
}

type ExposeConfig struct {
	Checks bool
	Paths  []ExposePath
}

type ExposePath struct {
	ListenerPort    int
	Path            string
	LocalPathPort   int
	Protocol        string
	ParsedFromCheck bool
}

type MeshGatewayMode string

type MeshGatewayConfig struct {
	Mode MeshGatewayMode
}

type UpstreamDestType string

type Upstream struct {
	DestinationType      UpstreamDestType
	DestinationNamespace string
	DestinationName      string
	Datacenter           string
	LocalBindAddress     string
	LocalBindPort        int
	Config               map[string]interface{}
	MeshGateway          MeshGatewayConfig
}
