package api

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

// DiscoveryChain can be used to query the discovery-chain endpoints
type DiscoveryChain struct {
	c *Client
}

// DiscoveryChain returns a handle to the discovery-chain endpoints
func (c *Client) DiscoveryChain() *DiscoveryChain {
	return &DiscoveryChain{c}
}

func (d *DiscoveryChain) Get(name string, opts *DiscoveryChainOptions, q *QueryOptions) (*DiscoveryChainResponse, *QueryMeta, error) {
	if name == "" {
		return nil, nil, fmt.Errorf("Name parameter must not be empty")
	}

	method := "GET"
	if opts != nil && opts.requiresPOST() {
		method = "POST"
	}

	r := d.c.newRequest(method, fmt.Sprintf("/v1/discovery-chain/%s", name))
	r.setQueryOptions(q)

	if opts != nil {
		if opts.EvaluateInDatacenter != "" {
			r.params.Set("compile-dc", opts.EvaluateInDatacenter)
		}
	}

	if method == "POST" {
		r.obj = opts
	}
	rtt, resp, err := d.c.doRequest(r)
	if err != nil {
		return nil, nil, err
	}
	defer closeResponseBody(resp)
	if err := requireOK(resp); err != nil {
		return nil, nil, err
	}

	qm := &QueryMeta{}
	parseQueryMeta(resp, qm)
	qm.RequestTime = rtt

	var out DiscoveryChainResponse

	if err := decodeBody(resp, &out); err != nil {
		return nil, nil, err
	}

	return &out, qm, nil
}

type DiscoveryChainOptions struct {
	EvaluateInDatacenter string `json:"-"`

	// OverrideMeshGateway allows for the mesh gateway setting to be overridden
	// for any resolver in the compiled chain.
	OverrideMeshGateway MeshGatewayConfig `json:",omitempty"`

	// OverrideProtocol allows for the final protocol for the chain to be
	// altered.
	//
	// - If the chain ordinarily would be TCP and an L7 protocol is passed here
	// the chain will not include Routers or Splitters.
	//
	// - If the chain ordinarily would be L7 and TCP is passed here the chain
	// will not include Routers or Splitters.
	OverrideProtocol string `json:",omitempty"`

	// OverrideConnectTimeout allows for the ConnectTimeout setting to be
	// overridden for any resolver in the compiled chain.
	OverrideConnectTimeout time.Duration `json:",omitempty"`
}

func (o *DiscoveryChainOptions) requiresPOST() bool {
	if o == nil {
		return false
	}
	return o.OverrideMeshGateway.Mode != "" ||
		o.OverrideProtocol != "" ||
		o.OverrideConnectTimeout != 0
}

type DiscoveryChainResponse struct {
	Chain *CompiledDiscoveryChain
}

type CompiledDiscoveryChain struct {
	ServiceName string
	Namespace   string
	Datacenter  string

	// CustomizationHash is a unique hash of any data that affects the
	// compilation of the discovery chain other than config entries or the
	// name/namespace/datacenter evaluation criteria.
	//
	// If set, this value should be used to prefix/suffix any generated load
	// balancer data plane objects to avoid sharing customized and
	// non-customized versions.
	CustomizationHash string

	// Default indicates if this discovery chain is based on no
	// service-resolver, service-splitter, or service-router config entries.
	Default bool

	// Protocol is the overall protocol shared by everything in the chain.
	Protocol string

	// ServiceMeta is the metadata from the underlying service-defaults config
	// entry for the service named ServiceName.
	ServiceMeta map[string]string

	// StartNode is the first key into the Nodes map that should be followed
	// when walking the discovery chain.
	StartNode string

	// Nodes contains all nodes available for traversal in the chain keyed by a
	// unique name.  You can walk this by starting with StartNode.
	//
	// NOTE: The names should be treated as opaque values and are only
	// guaranteed to be consistent within a single compilation.
	Nodes map[string]*DiscoveryGraphNode

	// Targets is a list of all targets used in this chain.
	//
	// NOTE: The names should be treated as opaque values and are only
	// guaranteed to be consistent within a single compilation.
	Targets map[string]*DiscoveryTarget
}

const (
	DiscoveryGraphNodeTypeRouter   = "router"
	DiscoveryGraphNodeTypeSplitter = "splitter"
	DiscoveryGraphNodeTypeResolver = "resolver"
)

// DiscoveryGraphNode is a single node in the compiled discovery chain.
type DiscoveryGraphNode struct {
	Type string
	Name string // this is NOT necessarily a service

	// fields for Type==router
	Routes []*DiscoveryRoute

	// fields for Type==splitter
	Splits []*DiscoverySplit

	// fields for Type==resolver
	Resolver *DiscoveryResolver

	// shared by Type==resolver || Type==splitter
	LoadBalancer *LoadBalancer `json:",omitempty"`
}

// compiled form of ServiceRoute
type DiscoveryRoute struct {
	Definition *ServiceRoute
	NextNode   string
}

// compiled form of ServiceSplit
type DiscoverySplit struct {
	Weight   float32
	NextNode string
}

// compiled form of ServiceResolverConfigEntry
type DiscoveryResolver struct {
	Default        bool
	ConnectTimeout time.Duration
	Target         string
	Failover       *DiscoveryFailover
}

func (r *DiscoveryResolver) MarshalJSON() ([]byte, error) {
	type Alias DiscoveryResolver
	exported := &struct {
		ConnectTimeout string `json:",omitempty"`
		*Alias
	}{
		ConnectTimeout: r.ConnectTimeout.String(),
		Alias:          (*Alias)(r),
	}
	if r.ConnectTimeout == 0 {
		exported.ConnectTimeout = ""
	}

	return json.Marshal(exported)
}

func (r *DiscoveryResolver) UnmarshalJSON(data []byte) error {
	type Alias DiscoveryResolver
	aux := &struct {
		ConnectTimeout string
		*Alias
	}{
		Alias: (*Alias)(r),
	}
	if err := json.Unmarshal(data, &aux); err != nil {
		return err
	}
	var err error
	if aux.ConnectTimeout != "" {
		if r.ConnectTimeout, err = time.ParseDuration(aux.ConnectTimeout); err != nil {
			return err
		}
	}
	return nil
}

// compiled form of ServiceResolverFailover
type DiscoveryFailover struct {
	Targets []string
}

// DiscoveryTarget represents all of the inputs necessary to use a resolver
// config entry to execute a catalog query to generate a list of service
// instances during discovery.
type DiscoveryTarget struct {
	ID string

	Service       string
	ServiceSubset string
	Namespace     string
	Datacenter    string

	MeshGateway    MeshGatewayConfig
	Subset         ServiceResolverSubset
	ConnectTimeout time.Duration
	External       bool
	SNI            string
	Name           string
}

func (t *DiscoveryTarget) MarshalJSON() ([]byte, error) {
	type Alias DiscoveryTarget
	exported := &struct {
		ConnectTimeout string `json:",omitempty"`
		*Alias
	}{
		ConnectTimeout: t.ConnectTimeout.String(),
		Alias:          (*Alias)(t),
	}
	if t.ConnectTimeout == 0 {
		exported.ConnectTimeout = ""
	}

	return json.Marshal(exported)
}

func (t *DiscoveryTarget) UnmarshalJSON(data []byte) error {
	type Alias DiscoveryTarget
	aux := &struct {
		ConnectTimeout string
		*Alias
	}{
		Alias: (*Alias)(t),
	}
	if err := json.Unmarshal(data, &aux); err != nil {
		return err
	}
	var err error
	if aux.ConnectTimeout != "" {
		if t.ConnectTimeout, err = time.ParseDuration(aux.ConnectTimeout); err != nil {
			return err
		}
	}
	return nil
}
