package structs

import (
	"encoding/json"
	"fmt"
	"reflect"

	"github.com/hashicorp/go-multierror"
	"github.com/mitchellh/copystructure"
	"github.com/mitchellh/mapstructure"
	"github.com/mitchellh/reflectwalk"
)

// ServiceDefinition is used to JSON decode the Service definitions. For
// documentation on specific fields see NodeService which is better documented.
type ServiceDefinition struct {
	Kind              ServiceKind `json:",omitempty"`
	ID                string
	Name              string
	Tags              []string
	Address           string
	Meta              map[string]string
	Port              int
	Check             CheckType
	Checks            CheckTypes
	Weights           *Weights
	Token             string
	EnableTagOverride bool
	// DEPRECATED (ProxyDestination) - remove this when removing ProxyDestination
	// ProxyDestination is deprecated in favor of Proxy.DestinationServiceName
	ProxyDestination string `json:",omitempty"`

	// Proxy is the configuration set for Kind = connect-proxy. It is mandatory in
	// that case and an error to be set for any other kind. This config is part of
	// a proxy service definition and is distinct from but shares some fields with
	// the Connect.Proxy which configures a managed proxy as part of the actual
	// service's definition. This duplication is ugly but seemed better than the
	// alternative which was to re-use the same struct fields for both cases even
	// though the semantics are different and the non-shared fields make no sense
	// in the other case. ProxyConfig may be a more natural name here, but it's
	// confusing for the UX because one of the fields in ConnectProxyConfig is
	// also called just "Config"
	Proxy *ConnectProxyConfig

	Connect *ServiceConnect
}

func (s *ServiceDefinition) NodeService() *NodeService {
	ns := &NodeService{
		Kind:              s.Kind,
		ID:                s.ID,
		Service:           s.Name,
		Tags:              s.Tags,
		Address:           s.Address,
		Meta:              s.Meta,
		Port:              s.Port,
		Weights:           s.Weights,
		EnableTagOverride: s.EnableTagOverride,
	}
	if s.Connect != nil {
		ns.Connect = *s.Connect
	}
	if s.Proxy != nil {
		ns.Proxy = *s.Proxy
		// Ensure the Upstream type is defaulted
		for i := range ns.Proxy.Upstreams {
			if ns.Proxy.Upstreams[i].DestinationType == "" {
				ns.Proxy.Upstreams[i].DestinationType = UpstreamDestTypeService
			}
		}
	} else {
		// DEPRECATED (ProxyDestination) - remove this when removing ProxyDestination
		// Legacy convert ProxyDestination into a Proxy config
		ns.Proxy.DestinationServiceName = s.ProxyDestination
	}
	if ns.ID == "" && ns.Service != "" {
		ns.ID = ns.Service
	}
	return ns
}

// ConnectManagedProxy returns a ConnectManagedProxy from the ServiceDefinition
// if one is configured validly. Note that is may return nil if no proxy is
// configured and will also return nil error in this case too as it's an
// expected case. The error returned indicates that there was an attempt to
// configure a proxy made but that it was invalid input, e.g. invalid
// "exec_mode".
func (s *ServiceDefinition) ConnectManagedProxy() (*ConnectManagedProxy, error) {
	if s.Connect == nil || s.Connect.Proxy == nil {
		return nil, nil
	}

	// NodeService performs some simple normalization like copying ID from Name
	// which we shouldn't hard code ourselves here...
	ns := s.NodeService()

	execMode, err := NewProxyExecMode(s.Connect.Proxy.ExecMode)
	if err != nil {
		return nil, err
	}

	// If upstreams were set in the config and NOT in the actual Upstreams field,
	// extract them out to the new explicit Upstreams and unset in config to make
	// transition smooth.
	if deprecatedUpstreams, ok := s.Connect.Proxy.Config["upstreams"]; ok {
		if len(s.Connect.Proxy.Upstreams) == 0 {
			if slice, ok := deprecatedUpstreams.([]interface{}); ok {
				for _, raw := range slice {
					var oldU deprecatedBuiltInProxyUpstreamConfig
					var decMeta mapstructure.Metadata
					decCfg := &mapstructure.DecoderConfig{
						Metadata: &decMeta,
						Result:   &oldU,
					}
					dec, err := mapstructure.NewDecoder(decCfg)
					if err != nil {
						// Just skip it - we never used to parse this so never failed
						// invalid stuff till it hit the proxy. This is a best-effort
						// attempt to not break existing service definitions so it's not the
						// end of the world if we don't have exactly the same failure mode
						// for invalid input.
						continue
					}
					err = dec.Decode(raw)
					if err != nil {
						// same logic as above
						continue
					}

					newT := UpstreamDestTypeService
					if oldU.DestinationType == "prepared_query" {
						newT = UpstreamDestTypePreparedQuery
					}
					u := Upstream{
						DestinationType:      newT,
						DestinationName:      oldU.DestinationName,
						DestinationNamespace: oldU.DestinationNamespace,
						Datacenter:           oldU.DestinationDatacenter,
						LocalBindAddress:     oldU.LocalBindAddress,
						LocalBindPort:        oldU.LocalBindPort,
					}
					// Any unrecognized keys should be copied into the config map
					if len(decMeta.Unused) > 0 {
						u.Config = make(map[string]interface{})
						// Paranoid type assertion - mapstructure would have errored if this
						// wasn't safe but panics are bad...
						if rawMap, ok := raw.(map[string]interface{}); ok {
							for _, k := range decMeta.Unused {
								u.Config[k] = rawMap[k]
							}
						}
					}
					s.Connect.Proxy.Upstreams = append(s.Connect.Proxy.Upstreams, u)
				}
			}
		}
		// Remove upstreams even if we didn't add them for consistency.
		delete(s.Connect.Proxy.Config, "upstreams")
	}

	p := &ConnectManagedProxy{
		ExecMode:  execMode,
		Command:   s.Connect.Proxy.Command,
		Config:    s.Connect.Proxy.Config,
		Upstreams: s.Connect.Proxy.Upstreams,
		// ProxyService will be setup when the agent registers the configured
		// proxies and starts them etc.
		TargetServiceID: ns.ID,
	}

	// Ensure the Upstream type is defaulted
	for i := range p.Upstreams {
		if p.Upstreams[i].DestinationType == "" {
			p.Upstreams[i].DestinationType = UpstreamDestTypeService
		}
	}

	return p, nil
}

// deprecatedBuiltInProxyUpstreamConfig is a struct for extracting old
// connect/proxy.UpstreamConfiguration syntax upstreams from existing managed
// proxy configs to convert them to new first-class Upstreams.
type deprecatedBuiltInProxyUpstreamConfig struct {
	LocalBindAddress      string `json:"local_bind_address" hcl:"local_bind_address,attr" mapstructure:"local_bind_address"`
	LocalBindPort         int    `json:"local_bind_port" hcl:"local_bind_port,attr" mapstructure:"local_bind_port"`
	DestinationName       string `json:"destination_name" hcl:"destination_name,attr" mapstructure:"destination_name"`
	DestinationNamespace  string `json:"destination_namespace" hcl:"destination_namespace,attr" mapstructure:"destination_namespace"`
	DestinationType       string `json:"destination_type" hcl:"destination_type,attr" mapstructure:"destination_type"`
	DestinationDatacenter string `json:"destination_datacenter" hcl:"destination_datacenter,attr" mapstructure:"destination_datacenter"`
	// ConnectTimeoutMs is removed explicitly because any additional config we
	// find including this field should be put into the opaque Config map in
	// Upstream.
}

// Validate validates the service definition. This also calls the underlying
// Validate method on the NodeService.
//
// NOTE(mitchellh): This currently only validates fields related to Connect
// and is incomplete with regards to other fields.
func (s *ServiceDefinition) Validate() error {
	var result error

	if s.Kind == ServiceKindTypical {
		if s.Connect != nil {
			if s.Connect.Proxy != nil {
				if s.Connect.Native {
					result = multierror.Append(result, fmt.Errorf(
						"Services that are Connect native may not have a proxy configuration"))
				}

				if s.Port == 0 {
					result = multierror.Append(result, fmt.Errorf(
						"Services with a Connect managed proxy must have a port set"))
				}
			}
		}
	}

	// Validate the NodeService which covers a lot
	if err := s.NodeService().Validate(); err != nil {
		result = multierror.Append(result, err)
	}

	return result
}

func (s *ServiceDefinition) CheckTypes() (checks CheckTypes, err error) {
	if !s.Check.Empty() {
		err := s.Check.Validate()
		if err != nil {
			return nil, err
		}
		checks = append(checks, &s.Check)
	}
	for _, check := range s.Checks {
		if err := check.Validate(); err != nil {
			return nil, err
		}
		checks = append(checks, check)
	}
	return checks, nil
}

// ServiceDefinitionConnectProxy is the connect proxy config  within a service
// registration. Note this is duplicated in config.ServiceConnectProxy and needs
// to be kept in sync.
type ServiceDefinitionConnectProxy struct {
	Command   []string               `json:",omitempty"`
	ExecMode  string                 `json:",omitempty"`
	Config    map[string]interface{} `json:",omitempty"`
	Upstreams []Upstream             `json:",omitempty"`
}

func (p *ServiceDefinitionConnectProxy) MarshalJSON() ([]byte, error) {
	type typeCopy ServiceDefinitionConnectProxy
	copy := typeCopy(*p)

	// If we have config, then we want to run it through our proxyConfigWalker
	// which is a reflectwalk implementation that attempts to turn arbitrary
	// interface{} values into JSON-safe equivalents (more or less). This
	// should always work because the config input is either HCL or JSON and
	// both are JSON compatible.
	if copy.Config != nil {
		configCopyRaw, err := copystructure.Copy(copy.Config)
		if err != nil {
			return nil, err
		}
		configCopy, ok := configCopyRaw.(map[string]interface{})
		if !ok {
			// This should never fail because we KNOW the input type,
			// but we don't ever want to risk the panic.
			return nil, fmt.Errorf("internal error: config copy is not right type")
		}
		if err := reflectwalk.Walk(configCopy, &proxyConfigWalker{}); err != nil {
			return nil, err
		}

		copy.Config = configCopy
	}

	return json.Marshal(&copy)
}

var typMapIfaceIface = reflect.TypeOf(map[interface{}]interface{}{})

// proxyConfigWalker implements interfaces for the reflectwalk package
// (github.com/mitchellh/reflectwalk) that can be used to automatically
// make the proxy configuration safe for JSON usage.
//
// Most of the implementation here is just keeping track of where we are
// in the reflectwalk process, so that we can replace values. The key logic
// is in Slice() and SliceElem().
//
// In particular we're looking to replace two cases the msgpack codec causes:
//
//   1.) String values get turned into byte slices. JSON will base64-encode
//       this and we don't want that, so we convert them back to strings.
//
//   2.) Nested maps turn into map[interface{}]interface{}. JSON cannot
//       encode this, so we need to turn it back into map[string]interface{}.
//
// This is tested via the TestServiceDefinitionConnectProxy_json test.
type proxyConfigWalker struct {
	lastValue    reflect.Value        // lastValue of map, required for replacement
	loc, lastLoc reflectwalk.Location // locations
	cs           []reflect.Value      // container stack
	csKey        []reflect.Value      // container keys (maps) stack
	csData       interface{}          // current container data
	sliceIndex   []int                // slice index stack (one for each slice in cs)
}

func (w *proxyConfigWalker) Enter(loc reflectwalk.Location) error {
	w.lastLoc = w.loc
	w.loc = loc
	return nil
}

func (w *proxyConfigWalker) Exit(loc reflectwalk.Location) error {
	w.loc = reflectwalk.None
	w.lastLoc = reflectwalk.None

	switch loc {
	case reflectwalk.Map:
		w.cs = w.cs[:len(w.cs)-1]
	case reflectwalk.MapValue:
		w.csKey = w.csKey[:len(w.csKey)-1]
	case reflectwalk.Slice:
		// Split any values that need to be split
		w.cs = w.cs[:len(w.cs)-1]
	case reflectwalk.SliceElem:
		w.csKey = w.csKey[:len(w.csKey)-1]
		w.sliceIndex = w.sliceIndex[:len(w.sliceIndex)-1]
	}

	return nil
}

func (w *proxyConfigWalker) Map(m reflect.Value) error {
	w.cs = append(w.cs, m)
	return nil
}

func (w *proxyConfigWalker) MapElem(m, k, v reflect.Value) error {
	w.csData = k
	w.csKey = append(w.csKey, k)

	w.lastValue = v
	return nil
}

func (w *proxyConfigWalker) Slice(v reflect.Value) error {
	// If we find a []byte slice, it is an HCL-string converted to []byte.
	// Convert it back to a Go string and replace the value so that JSON
	// doesn't base64-encode it.
	if v.Type() == reflect.TypeOf([]byte{}) {
		resultVal := reflect.ValueOf(string(v.Interface().([]byte)))
		switch w.lastLoc {
		case reflectwalk.MapKey:
			m := w.cs[len(w.cs)-1]

			// Delete the old value
			var zero reflect.Value
			m.SetMapIndex(w.csData.(reflect.Value), zero)

			// Set the new key with the existing value
			m.SetMapIndex(resultVal, w.lastValue)

			// Set the key to be the new key
			w.csData = resultVal
		case reflectwalk.MapValue:
			// If we're in a map, then the only way to set a map value is
			// to set it directly.
			m := w.cs[len(w.cs)-1]
			mk := w.csData.(reflect.Value)
			m.SetMapIndex(mk, resultVal)
		case reflectwalk.Slice:
			s := w.cs[len(w.cs)-1]
			s.Index(w.sliceIndex[len(w.sliceIndex)-1]).Set(resultVal)
		default:
			return fmt.Errorf("cannot convert []byte")
		}
	}

	w.cs = append(w.cs, v)
	return nil
}

func (w *proxyConfigWalker) SliceElem(i int, elem reflect.Value) error {
	w.csKey = append(w.csKey, reflect.ValueOf(i))
	w.sliceIndex = append(w.sliceIndex, i)

	// We're looking specifically for map[interface{}]interface{}, but the
	// values in a slice are wrapped up in interface{} so we need to unwrap
	// that first. Therefore, we do three checks: 1.) is it valid? so we
	// don't panic, 2.) is it an interface{}? so we can unwrap it and 3.)
	// after unwrapping the interface do we have the map we expect?
	if !elem.IsValid() {
		return nil
	}

	if elem.Kind() != reflect.Interface {
		return nil
	}

	if inner := elem.Elem(); inner.Type() == typMapIfaceIface {
		// map[interface{}]interface{}, attempt to weakly decode into string keys
		var target map[string]interface{}
		if err := mapstructure.WeakDecode(inner.Interface(), &target); err != nil {
			return err
		}

		elem.Set(reflect.ValueOf(target))
	}

	return nil
}
