/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 dubbo

import (
	"bytes"
	"encoding/json"
	"io"
	"net/url"
	"reflect"
	"strconv"
	"strings"
	"time"
)

import (
	"github.com/dubbo-go-pixiu/pixiu-api/pkg/api/config"

	"github.com/pkg/errors"

	"github.com/spf13/cast"
)

import (
	"github.com/apache/dubbo-go-pixiu/pkg/client"
	"github.com/apache/dubbo-go-pixiu/pkg/common/constant"
	"github.com/apache/dubbo-go-pixiu/pkg/router"
)

var mappers = map[string]client.ParamMapper{
	constant.QueryStrings: queryStringsMapper{},
	constant.Headers:      headerMapper{},
	constant.RequestBody:  bodyMapper{},
	constant.RequestURI:   uriMapper{},
}

type dubboTarget struct {
	Values []any    // the slice contains the parameters.
	Types  []string // the slice contains the parameters' types. It should match the values one by one.
}

// pre-allocate proper memory according to the params' usability.
func newDubboTarget(mps []config.MappingParam) *dubboTarget {
	length := 0

	for i := 0; i < len(mps); i++ {
		isGeneric, v := getGenericMapTo(mps[i].MapTo)
		if isGeneric && v != optionKeyValues {
			continue
		}
		length++
	}

	if length > 0 {
		val := make([]any, length)
		target := &dubboTarget{
			Values: val,
			Types:  make([]string, length),
		}
		return target
	}
	return nil
}

type queryStringsMapper struct{}

// nolint
func (qm queryStringsMapper) Map(mp config.MappingParam, c *client.Request, target any, option client.RequestOption) error {
	t, err := validateTarget(target)
	if err != nil {
		return err
	}
	queryValues, err := url.ParseQuery(c.IngressRequest.URL.RawQuery)
	if err != nil {
		return errors.Wrap(err, "Error happened when parsing the query paramters")
	}
	_, key, err := client.ParseMapSource(mp.Name)
	if err != nil {
		return err
	}
	pos, err := strconv.Atoi(mp.MapTo)
	if err != nil && option == nil {
		return errors.Errorf("Parameter mapping %v incorrect", mp)
	}
	qValue := queryValues.Get(key[0])
	if len(qValue) == 0 {
		return errors.Errorf("Query parameter %s does not exist", key)
	}

	return setTargetWithOpt(c, option, t, pos, qValue, mp.MapType)
}

type headerMapper struct{}

// nolint
func (hm headerMapper) Map(mp config.MappingParam, c *client.Request, target any, option client.RequestOption) error {
	rv, err := validateTarget(target)
	if err != nil {
		return err
	}
	_, key, err := client.ParseMapSource(mp.Name)
	pos, err := strconv.Atoi(mp.MapTo)
	if err != nil && option == nil {
		return errors.Errorf("Parameter mapping %+v incorrect", mp)
	}
	header := c.IngressRequest.Header.Get(key[0])
	if len(header) == 0 {
		return errors.Errorf("Header %s not found", key[0])
	}

	return setTargetWithOpt(c, option, rv, pos, header, mp.MapType)
}

type bodyMapper struct{}

// nolint
func (bm bodyMapper) Map(mp config.MappingParam, c *client.Request, target any, option client.RequestOption) error {
	// TO-DO: add support for content-type other than application/json
	rv, err := validateTarget(target)
	if err != nil {
		return err
	}
	_, keys, err := client.ParseMapSource(mp.Name)
	if err != nil {
		return err
	}
	pos, err := strconv.Atoi(mp.MapTo)
	if err != nil && option == nil {
		return errors.Errorf("Parameter mapping %v incorrect, parameters for Dubbo backend must be mapped to an int to represent position", mp)
	}

	rawBody, err := io.ReadAll(c.IngressRequest.Body)
	defer func() {
		c.IngressRequest.Body = io.NopCloser(bytes.NewReader(rawBody))
	}()
	if err != nil {
		return err
	}
	mapBody := map[string]any{}
	json.Unmarshal(rawBody, &mapBody)
	val, err := client.GetMapValue(mapBody, keys)

	if err := setTargetWithOpt(c, option, rv, pos, val, mp.MapType); err != nil {
		return errors.Wrap(err, "set target fail")
	}

	c.IngressRequest.Body = io.NopCloser(bytes.NewBuffer(rawBody))
	return nil
}

type uriMapper struct{}

// nolint
func (um uriMapper) Map(mp config.MappingParam, c *client.Request, target any, option client.RequestOption) error {
	rv, err := validateTarget(target)
	if err != nil {
		return err
	}
	_, keys, err := client.ParseMapSource(mp.Name)
	if err != nil {
		return err
	}
	pos, err := strconv.Atoi(mp.MapTo)
	if err != nil && option == nil {
		return errors.Errorf("Parameter mapping %v incorrect", mp)
	}
	uriValues := router.GetURIParams(&c.API, *c.IngressRequest.URL)

	return setTargetWithOpt(c, option, rv, pos, uriValues.Get(keys[0]), mp.MapType)
}

// validateTarget verify if the incoming target for the Map function
// can be processed as expected.
func validateTarget(target any) (*dubboTarget, error) {
	val, ok := target.(*dubboTarget)
	if !ok {
		return nil, errors.New("Target params for dubbo backend must be *dubboTarget")
	}
	return val, nil
}

func setTargetWithOpt(req *client.Request, option client.RequestOption,
	target *dubboTarget, pos int, value any, targetType string) error {
	if option != nil {
		return setGenericTarget(req, option, target, value, targetType)
	}
	value, err := mapTypes(targetType, value)
	if err != nil {
		return err
	}
	setCommonTarget(target, pos, value, targetType)
	return nil
}

func setGenericTarget(req *client.Request, option client.RequestOption,
	target *dubboTarget, value any, targetType string) error {
	var err error
	switch option.(type) {
	case *groupOpt, *versionOpt, *interfaceOpt, *applicationOpt, *methodOpt:
		err = option.Action(req, value)
	case *valuesOpt:
		err = option.Action(target, [2]any{value, targetType})
	case *paramTypesOpt:
		err = option.Action(target, value)
	}
	return err
}

func setCommonTarget(target *dubboTarget, pos int, value any, targetType string) {
	// if the mapTo position is greater than the numbers of usable parameters,
	// extend the values and types slices. It changes the address of the the target.
	if cap(target.Values) <= pos {
		list := make([]any, pos+1-len(target.Values))
		typeList := make([]string, pos+1-len(target.Types))
		target.Values = append(target.Values, list...)
		target.Types = append(target.Types, typeList...)
	}
	target.Values[pos] = value
	target.Types[pos] = targetType
}

func mapTypes(jType string, originVal any) (any, error) {
	targetType, ok := constant.JTypeMapper[jType]
	if !ok {
		return nil, errors.Errorf("Invalid parameter type: %s", jType)
	}
	switch targetType {
	case reflect.TypeOf(""):
		return cast.ToStringE(originVal)
	case reflect.TypeOf(int(0)):
		return cast.ToIntE(originVal)
	case reflect.TypeOf(int8(0)):
		return cast.ToInt8E(originVal)
	case reflect.TypeOf(int16(16)):
		return cast.ToInt16E(originVal)
	case reflect.TypeOf(int32(0)):
		return cast.ToInt32E(originVal)
	case reflect.TypeOf(int64(0)):
		return cast.ToInt64E(originVal)
	case reflect.TypeOf(float32(0)):
		return cast.ToFloat32E(originVal)
	case reflect.TypeOf(float64(0)):
		return cast.ToFloat64E(originVal)
	case reflect.TypeOf(true):
		return cast.ToBoolE(originVal)
	case reflect.TypeOf(time.Time{}):
		return cast.ToTimeE(originVal)
	default:
		return originVal, nil
	}
}

// getGenericMapTo will parse the mapTo field, if the mapTo value is
// opt.xxx, the "opt." prefix will identify the param mapTo generic field,
// supporting generic fields: interface, group, application, method, version,
// values, types
func getGenericMapTo(mapTo string) (isGeneric bool, genericField string) {
	fields := strings.Split(mapTo, ".")
	if len(fields) != 2 || fields[0] != "opt" {
		return false, ""
	}
	if _, ok := DefaultMapOption[fields[1]]; !ok {
		return false, ""
	}
	return true, fields[1]
}
