/*
 *
 * 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 xdsrouting

import (
	"encoding/json"
	"fmt"

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

type actionConfig struct {
	// ChildPolicy is the child policy and it's config.
	ChildPolicy *internalserviceconfig.BalancerConfig
}

type int64Range struct {
	start, end int64
}

type headerMatcher struct {
	name string
	// matchSpecifiers
	invertMatch bool

	// At most one of the following has non-default value.
	exactMatch, regexMatch, prefixMatch, suffixMatch string
	rangeMatch                                       *int64Range
	presentMatch                                     bool
}

type routeConfig struct {
	// Path, Prefix and Regex can have at most one set. This is guaranteed by
	// config parsing.
	path, prefix, regex string

	headers  []headerMatcher
	fraction *uint32

	// Action is the name from the action list.
	action string
}

// lbConfig is the balancer config for xds routing policy.
type lbConfig struct {
	serviceconfig.LoadBalancingConfig
	routes  []routeConfig
	actions map[string]actionConfig
}

// The following structs with `JSON` in name are temporary structs to unmarshal
// json into. The fields will be read into lbConfig, to be used by the balancer.

// routeJSON is temporary struct for json unmarshal.
type routeJSON struct {
	// Path, Prefix and Regex can have at most one non-nil.
	Path, Prefix, Regex *string
	// Zero or more header matchers.
	Headers       []*xdsclient.HeaderMatcher
	MatchFraction *wrapperspb.UInt32Value
	// Action is the name from the action list.
	Action string
}

// lbConfigJSON is temporary struct for json unmarshal.
type lbConfigJSON struct {
	Route  []routeJSON
	Action map[string]actionConfig
}

func (jc lbConfigJSON) toLBConfig() *lbConfig {
	var ret lbConfig
	for _, r := range jc.Route {
		var tempR routeConfig
		switch {
		case r.Path != nil:
			tempR.path = *r.Path
		case r.Prefix != nil:
			tempR.prefix = *r.Prefix
		case r.Regex != nil:
			tempR.regex = *r.Regex
		}
		for _, h := range r.Headers {
			var tempHeader headerMatcher
			switch {
			case h.ExactMatch != nil:
				tempHeader.exactMatch = *h.ExactMatch
			case h.RegexMatch != nil:
				tempHeader.regexMatch = *h.RegexMatch
			case h.PrefixMatch != nil:
				tempHeader.prefixMatch = *h.PrefixMatch
			case h.SuffixMatch != nil:
				tempHeader.suffixMatch = *h.SuffixMatch
			case h.RangeMatch != nil:
				tempHeader.rangeMatch = &int64Range{
					start: h.RangeMatch.Start,
					end:   h.RangeMatch.End,
				}
			case h.PresentMatch != nil:
				tempHeader.presentMatch = *h.PresentMatch
			}
			tempHeader.name = h.Name
			if h.InvertMatch != nil {
				tempHeader.invertMatch = *h.InvertMatch
			}
			tempR.headers = append(tempR.headers, tempHeader)
		}
		if r.MatchFraction != nil {
			tempR.fraction = &r.MatchFraction.Value
		}
		tempR.action = r.Action
		ret.routes = append(ret.routes, tempR)
	}
	ret.actions = jc.Action
	return &ret
}

func parseConfig(c json.RawMessage) (*lbConfig, error) {
	var tempConfig lbConfigJSON
	if err := json.Unmarshal(c, &tempConfig); err != nil {
		return nil, err
	}

	// For each route:
	// - at most one of path/prefix/regex.
	// - action is in action list.

	allRouteActions := make(map[string]bool)
	for _, r := range tempConfig.Route {
		var oneOfCount int
		if r.Path != nil {
			oneOfCount++
		}
		if r.Prefix != nil {
			oneOfCount++
		}
		if r.Regex != nil {
			oneOfCount++
		}
		if oneOfCount != 1 {
			return nil, fmt.Errorf("%d (not exactly one) of path/prefix/regex is set in route %+v", oneOfCount, r)
		}

		for _, h := range r.Headers {
			var oneOfCountH int
			if h.ExactMatch != nil {
				oneOfCountH++
			}
			if h.RegexMatch != nil {
				oneOfCountH++
			}
			if h.PrefixMatch != nil {
				oneOfCountH++
			}
			if h.SuffixMatch != nil {
				oneOfCountH++
			}
			if h.RangeMatch != nil {
				oneOfCountH++
			}
			if h.PresentMatch != nil {
				oneOfCountH++
			}
			if oneOfCountH != 1 {
				return nil, fmt.Errorf("%d (not exactly one) of header matcher specifier is set in route %+v", oneOfCountH, h)
			}
		}

		if _, ok := tempConfig.Action[r.Action]; !ok {
			return nil, fmt.Errorf("action %q from route %+v is not found in action list", r.Action, r)
		}
		allRouteActions[r.Action] = true
	}

	// Verify that actions are used by at least one route.
	for n := range tempConfig.Action {
		if _, ok := allRouteActions[n]; !ok {
			return nil, fmt.Errorf("action %q is not used by any route", n)
		}
	}

	return tempConfig.toLBConfig(), nil
}
