/*
 *
 * Copyright 2020 gRPC 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 resolver

import (
	"encoding/json"
	"fmt"

	wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
	xdsclient "google.golang.org/grpc/xds/internal/client"
)

const (
	cdsName            = "cds_experimental"
	weightedTargetName = "weighted_target_experimental"
	xdsRoutingName     = "xds_routing_experimental"
)

type serviceConfig struct {
	LoadBalancingConfig balancerConfig `json:"loadBalancingConfig"`
}

type balancerConfig []map[string]interface{}

func newBalancerConfig(name string, config interface{}) balancerConfig {
	return []map[string]interface{}{{name: config}}
}

type weightedCDSBalancerConfig struct {
	Targets map[string]cdsWithWeight `json:"targets"`
}

type cdsWithWeight struct {
	Weight      uint32         `json:"weight"`
	ChildPolicy balancerConfig `json:"childPolicy"`
}

type cdsBalancerConfig struct {
	Cluster string `json:"cluster"`
}

type route struct {
	Path     *string                    `json:"path,omitempty"`
	Prefix   *string                    `json:"prefix,omitempty"`
	Regex    *string                    `json:"regex,omitempty"`
	Headers  []*xdsclient.HeaderMatcher `json:"headers,omitempty"`
	Fraction *wrapperspb.UInt32Value    `json:"matchFraction,omitempty"`
	Action   string                     `json:"action"`
}

type xdsActionConfig struct {
	ChildPolicy balancerConfig `json:"childPolicy"`
}

type xdsRoutingBalancerConfig struct {
	Action map[string]xdsActionConfig `json:"action"`
	Route  []*route                   `json:"route"`
}

func (r *xdsResolver) routesToJSON(routes []*xdsclient.Route) (string, error) {
	r.updateActions(newActionsFromRoutes(routes))

	// Generate routes.
	var rts []*route
	for _, rt := range routes {
		t := &route{
			Path:    rt.Path,
			Prefix:  rt.Prefix,
			Regex:   rt.Regex,
			Headers: rt.Headers,
		}

		if f := rt.Fraction; f != nil {
			t.Fraction = &wrapperspb.UInt32Value{Value: *f}
		}

		t.Action = r.getActionAssignedName(rt.Action)
		rts = append(rts, t)
	}

	// Generate actions.
	action := make(map[string]xdsActionConfig)
	for _, act := range r.actions {
		action[act.assignedName] = xdsActionConfig{
			ChildPolicy: weightedClusterToBalancerConfig(act.clustersWithWeights),
		}
	}

	sc := serviceConfig{
		LoadBalancingConfig: newBalancerConfig(
			xdsRoutingName, xdsRoutingBalancerConfig{
				Route:  rts,
				Action: action,
			},
		),
	}

	bs, err := json.Marshal(sc)
	if err != nil {
		return "", fmt.Errorf("failed to marshal json: %v", err)
	}
	return string(bs), nil
}

func weightedClusterToBalancerConfig(wc map[string]uint32) balancerConfig {
	// Even if WeightedCluster has only one entry, we still use weighted_target
	// as top level balancer, to avoid switching top policy between CDS and
	// weighted_target, causing TCP connection to be recreated.
	targets := make(map[string]cdsWithWeight)
	for name, weight := range wc {
		targets[name] = cdsWithWeight{
			Weight:      weight,
			ChildPolicy: newBalancerConfig(cdsName, cdsBalancerConfig{Cluster: name}),
		}
	}
	bc := newBalancerConfig(
		weightedTargetName, weightedCDSBalancerConfig{
			Targets: targets,
		},
	)
	return bc
}

func (r *xdsResolver) serviceUpdateToJSON(su xdsclient.ServiceUpdate) (string, error) {
	return r.routesToJSON(su.Routes)
}
