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

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

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

	"github.com/pkg/errors"
)

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 requestParams struct {
	Header    http.Header
	Query     url.Values
	Body      io.ReadCloser
	URIParams url.Values
}

func newRequestParams() *requestParams {
	return &requestParams{
		Header:    http.Header{},
		Query:     url.Values{},
		Body:      io.NopCloser(bytes.NewReader([]byte(""))),
		URIParams: url.Values{},
	}
}

type queryStringsMapper struct{}

// nolint
func (qs queryStringsMapper) Map(mp config.MappingParam, c *client.Request, rawTarget any, option client.RequestOption) error {
	target, fromKey, to, toKey, err := mapPrepare(mp, rawTarget)
	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")
	}
	rawValue := queryValues.Get(fromKey[0])
	if len(rawValue) == 0 {
		return errors.Errorf("%s in query parameters not found", fromKey[0])
	}
	setTarget(target, to, toKey[0], rawValue)
	return nil
}

type headerMapper struct{}

// nolint
func (hm headerMapper) Map(mp config.MappingParam, c *client.Request, rawTarget any, option client.RequestOption) error {
	target, fromKey, to, toKey, err := mapPrepare(mp, rawTarget)
	if err != nil {
		return err
	}

	rawHeader := c.IngressRequest.Header.Get(fromKey[0])
	if len(rawHeader) == 0 {
		return errors.Errorf("Header %s not found", fromKey[0])
	}
	setTarget(target, to, toKey[0], rawHeader)
	return nil
}

type bodyMapper struct{}

// nolint
func (bm bodyMapper) Map(mp config.MappingParam, c *client.Request, rawTarget any, option client.RequestOption) error {
	// TO-DO: add support for content-type other than application/json
	target, fromKey, to, toKey, err := mapPrepare(mp, rawTarget)
	if err != nil {
		return err
	}

	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, fromKey)
	if err != nil {
		return errors.Wrapf(err, "Error when get body value from key %s", fromKey)
	}
	setTarget(target, to, strings.Join(toKey, constant.Dot), val)
	return nil
}

type uriMapper struct{}

// nolint
func (um uriMapper) Map(mp config.MappingParam, c *client.Request, rawTarget any, option client.RequestOption) error {
	target, fromKey, to, toKey, err := mapPrepare(mp, rawTarget)
	if err != nil {
		return err
	}
	if to == constant.RequestURI {
	}
	uriValues := router.GetURIParams(&c.API, *c.IngressRequest.URL)
	if uriValues == nil {
		return errors.New("No URI parameters found")
	}
	setTarget(target, to, strings.Join(toKey, constant.Dot), uriValues.Get(fromKey[0]))
	return nil
}

func validateTarget(target any) (*requestParams, error) {
	val, ok := target.(*requestParams)
	if !ok || val == nil {
		return nil, errors.New("Target params must be a requestParams pointer")
	}
	return val, nil
}

func setTarget(target *requestParams, to string, key string, val any) error {
	valType := reflect.TypeOf(val)
	if (to == constant.Headers || to == constant.QueryStrings) && valType.Kind() != reflect.String {
		return errors.Errorf("%s only accepts string", to)
	}
	switch to {
	case constant.Headers:
		target.Header.Set(key, val.(string))
	case constant.RequestURI:
		target.URIParams.Set(key, val.(string))
	case constant.QueryStrings:
		target.Query.Set(key, val.(string))
	case constant.RequestBody:
		rawBody, err := io.ReadAll(target.Body)
		defer func() {
			target.Body = io.NopCloser(bytes.NewReader(rawBody))
		}()
		if err != nil {
			return errors.New("Raw body read failed")
		}
		mapBody := map[string]any{}
		if len(rawBody) != 0 {
			err = json.Unmarshal(rawBody, &mapBody)
			if err != nil {
				return errors.New("Raw body parse failed")
			}
		}
		setMapWithPath(mapBody, key, val)
		rawBody, err = json.Marshal(mapBody)
		if err != nil {
			return errors.New("Stringify map to body failed")
		}
	default:
		return errors.Errorf("Mapping target to %s does not support", to)
	}
	return nil
}

// setMapWithPath set the value to the target map. If the origin targetMap has
// {"abc": "cde": {"f":1, "g":2}} and the path is abc, value is "123", then the
// targetMap will be updated to {"abc", "123"}
func setMapWithPath(targetMap map[string]any, path string, val any) map[string]any {
	keys := strings.Split(path, constant.Dot)

	_, ok := targetMap[keys[0]]
	if len(keys) == 1 {
		targetMap[keys[0]] = val
		return targetMap
	}
	if !ok && len(keys) != 1 {
		targetMap[keys[0]] = make(map[string]any)
		targetMap[keys[0]] = setMapWithPath(targetMap[keys[0]].(map[string]any), strings.Join(keys[1:], constant.Dot), val)
	}
	return targetMap
}

func mapPrepare(mp config.MappingParam, rawTarget any) (target *requestParams, fromKey []string, to string, toKey []string, err error) {
	// ensure the target is a pointer and type is requestParams
	target, err = validateTarget(rawTarget)
	if err != nil {
		return nil, nil, "", nil, err
	}
	// retrieve the mapping values' origin param name
	_, fromKey, err = client.ParseMapSource(mp.Name)
	if err != nil {
		return nil, nil, "", nil, err
	}
	// retrieve the mapping values' target param name and param types(header/uri/query/request body)
	to, toKey, err = client.ParseMapSource(mp.MapTo)
	if err != nil {
		return nil, nil, "", nil, err
	}
	return target, fromKey, to, toKey, nil
}
