package runtime

import (
	"encoding/json"
	"errors"
	"fmt"
	"mime/multipart"
	"net/url"
	"reflect"
	"strconv"
	"strings"

	"github.com/oapi-codegen/runtime/types"
)

const jsonTagName = "json"
const formTagName = "form"
const jsonContentType = "application/json"

// formFieldTag returns the struct tag value that names a field on the wire
// for form encoding. The `form` tag is preferred when present and non-empty;
// the `json` tag is the historical fallback, kept for compatibility with
// generated and hand-written structs that carry only json tags. The full tag
// value is returned, so `-` skips and the `,omitempty` option follow
// whichever tag was selected.
func formFieldTag(f reflect.StructField) string {
	if tag, ok := f.Tag.Lookup(formTagName); ok && tag != "" {
		return tag
	}
	return f.Tag.Get(jsonTagName)
}

type RequestBodyEncoding struct {
	ContentType string
	Style       string
	Explode     *bool
	Required    *bool
}

func BindMultipart(ptr interface{}, reader multipart.Reader) error {
	const defaultMemory = 32 << 20
	form, err := reader.ReadForm(defaultMemory)
	if err != nil {
		return err
	}
	return BindForm(ptr, form.Value, form.File, nil)
}

func BindForm(ptr interface{}, form map[string][]string, files map[string][]*multipart.FileHeader, encodings map[string]RequestBodyEncoding) error {
	ptrVal := reflect.Indirect(reflect.ValueOf(ptr))
	if ptrVal.Kind() != reflect.Struct {
		return errors.New("form data body should be a struct")
	}
	tValue := ptrVal.Type()

	for i := 0; i < tValue.NumField(); i++ {
		field := ptrVal.Field(i)
		tag := formFieldTag(tValue.Field(i))
		if !field.CanInterface() || tag == "-" {
			continue
		}
		tag = strings.Split(tag, ",")[0] // extract the name of the tag
		if encoding, ok := encodings[tag]; ok {
			// custom encoding
			values := form[tag]
			if len(values) == 0 {
				continue
			}
			value := values[0]
			if encoding.ContentType != "" {
				if strings.HasPrefix(encoding.ContentType, jsonContentType) {
					if err := json.Unmarshal([]byte(value), ptr); err != nil {
						return err
					}
				}
				return errors.New("unsupported encoding, only application/json is supported")
			} else {
				var explode bool
				if encoding.Explode != nil {
					explode = *encoding.Explode
				}
				var required bool
				if encoding.Required != nil {
					required = *encoding.Required
				}
				if err := BindStyledParameterWithOptions(encoding.Style, tag, value, field.Addr().Interface(), BindStyledParameterOptions{
					ParamLocation: ParamLocationUndefined,
					Explode:       explode,
					Required:      required,
				}); err != nil {
					return err
				}
			}
		} else {
			// regular form data
			if _, err := bindFormImpl(field, form, files, tag); err != nil {
				return err
			}
		}
	}

	return nil
}

func MarshalForm(ptr interface{}, encodings map[string]RequestBodyEncoding) (url.Values, error) {
	ptrVal := reflect.Indirect(reflect.ValueOf(ptr))
	if ptrVal.Kind() != reflect.Struct {
		return nil, errors.New("form data body should be a struct")
	}
	tValue := ptrVal.Type()
	result := make(url.Values)
	for i := 0; i < tValue.NumField(); i++ {
		field := ptrVal.Field(i)
		tag := formFieldTag(tValue.Field(i))
		if !field.CanInterface() || tag == "-" {
			continue
		}
		omitEmpty := strings.HasSuffix(tag, ",omitempty")
		if omitEmpty && field.IsZero() {
			continue
		}
		tag = strings.Split(tag, ",")[0] // extract the name of the tag
		if encoding, ok := encodings[tag]; ok && encoding.ContentType != "" {
			if strings.HasPrefix(encoding.ContentType, jsonContentType) {
				if data, err := json.Marshal(field); err != nil { //nolint:staticcheck
					return nil, err
				} else {
					result[tag] = append(result[tag], string(data))
				}
			}
			return nil, errors.New("unsupported encoding, only application/json is supported")
		} else {
			marshalFormImpl(field, result, tag)
		}
	}
	return result, nil
}

func bindFormImpl(v reflect.Value, form map[string][]string, files map[string][]*multipart.FileHeader, name string) (bool, error) {
	var hasData bool
	switch v.Kind() {
	case reflect.Interface:
		return bindFormImpl(v.Elem(), form, files, name)
	case reflect.Pointer:
		ptrData := v.Elem()
		if !ptrData.IsValid() {
			ptrData = reflect.New(v.Type().Elem())
		}
		ptrHasData, err := bindFormImpl(ptrData, form, files, name)
		if err == nil && ptrHasData && !v.Elem().IsValid() {
			v.Set(ptrData)
		}
		return ptrHasData, err
	case reflect.Slice:
		if files := append(files[name], files[name+"[]"]...); len(files) != 0 {
			if _, ok := v.Interface().([]types.File); ok {
				result := make([]types.File, len(files))
				for i, file := range files {
					result[i].InitFromMultipart(file)
				}
				v.Set(reflect.ValueOf(result))
				hasData = true
			}
		}
		indexedElementsCount := indexedElementsCount(form, files, name)
		items := append(form[name], form[name+"[]"]...)
		if indexedElementsCount+len(items) != 0 {
			result := reflect.MakeSlice(v.Type(), indexedElementsCount+len(items), indexedElementsCount+len(items))
			for i := 0; i < indexedElementsCount; i++ {
				if _, err := bindFormImpl(result.Index(i), form, files, fmt.Sprintf("%s[%v]", name, i)); err != nil {
					return false, err
				}
			}
			for i, item := range items {
				if err := BindStringToObject(item, result.Index(indexedElementsCount+i).Addr().Interface()); err != nil {
					return false, err
				}
			}
			v.Set(result)
			hasData = true
		}
	case reflect.Struct:
		if files := files[name]; len(files) != 0 {
			if file, ok := v.Interface().(types.File); ok {
				file.InitFromMultipart(files[0])
				v.Set(reflect.ValueOf(file))
				return true, nil
			}
		}
		for i := 0; i < v.NumField(); i++ {
			field := v.Type().Field(i)
			tag := formFieldTag(field)
			if field.Name == "AdditionalProperties" && field.Type.Kind() == reflect.Map && tag == "-" {
				additionalPropertiesHasData, err := bindAdditionalProperties(v.Field(i), v, form, files, name)
				if err != nil {
					return false, err
				}
				hasData = hasData || additionalPropertiesHasData
			}
			if !v.Field(i).CanInterface() || tag == "-" {
				continue
			}
			tag = strings.Split(tag, ",")[0] // extract the name of the tag
			fieldHasData, err := bindFormImpl(v.Field(i), form, files, fmt.Sprintf("%s[%s]", name, tag))
			if err != nil {
				return false, err
			}
			hasData = hasData || fieldHasData
		}
		return hasData, nil
	case reflect.Map:
		// A bool-keyed map (such as nullable.Nullable[T], which is
		// map[bool]T) is treated as a nullable wrapper: bind the inner type
		// and store the result under map[true]. An absent field stays as
		// the zero (unspecified) map.
		if v.Type().Key().Kind() == reflect.Bool {
			valuePtr := reflect.New(v.Type().Elem())
			valueHasData, err := bindFormImpl(valuePtr.Elem(), form, files, name)
			if err != nil {
				return false, err
			}
			if valueHasData {
				newMap := reflect.MakeMap(v.Type())
				newMap.SetMapIndex(reflect.ValueOf(true), valuePtr.Elem())
				v.Set(newMap)
				return true, nil
			}
			return false, nil
		}
		return bindFormMap(v, form, files, name)
	default:
		value := form[name]
		if len(value) != 0 {
			return true, BindStringToObject(value[0], v.Addr().Interface())
		}
	}
	return hasData, nil
}

func indexedElementsCount(form map[string][]string, files map[string][]*multipart.FileHeader, name string) int {
	name += "["
	maxIndex := -1
	for k := range form {
		if strings.HasPrefix(k, name) {
			str := strings.TrimPrefix(k, name)
			str = str[:strings.Index(str, "]")]
			if idx, err := strconv.Atoi(str); err == nil {
				if idx > maxIndex {
					maxIndex = idx
				}
			}
		}
	}
	for k := range files {
		if strings.HasPrefix(k, name) {
			str := strings.TrimPrefix(k, name)
			str = str[:strings.Index(str, "]")]
			if idx, err := strconv.Atoi(str); err == nil {
				if idx > maxIndex {
					maxIndex = idx
				}
			}
		}
	}
	return maxIndex + 1
}

func bindAdditionalProperties(additionalProperties reflect.Value, parentStruct reflect.Value, form map[string][]string, files map[string][]*multipart.FileHeader, name string) (bool, error) {
	hasData := false
	valueType := additionalProperties.Type().Elem()

	// store all fixed properties in a set
	fieldsSet := make(map[string]struct{})
	for i := 0; i < parentStruct.NumField(); i++ {
		tag := formFieldTag(parentStruct.Type().Field(i))
		if !parentStruct.Field(i).CanInterface() || tag == "-" {
			continue
		}
		tag = strings.Split(tag, ",")[0]
		fieldsSet[tag] = struct{}{}
	}

	result := reflect.MakeMap(additionalProperties.Type())
	for k := range form {
		if strings.HasPrefix(k, name+"[") {
			key := strings.TrimPrefix(k, name+"[")
			key = key[:strings.Index(key, "]")]
			if _, ok := fieldsSet[key]; ok {
				continue
			}
			value := reflect.New(valueType)
			ptrHasData, err := bindFormImpl(value, form, files, fmt.Sprintf("%s[%s]", name, key))
			if err != nil {
				return false, err
			}
			result.SetMapIndex(reflect.ValueOf(key), value.Elem())
			hasData = hasData || ptrHasData
		}
	}
	for k := range files {
		if strings.HasPrefix(k, name+"[") {
			key := strings.TrimPrefix(k, name+"[")
			key = key[:strings.Index(key, "]")]
			if _, ok := fieldsSet[key]; ok {
				continue
			}
			value := reflect.New(valueType)
			result.SetMapIndex(reflect.ValueOf(key), value)
			ptrHasData, err := bindFormImpl(value, form, files, fmt.Sprintf("%s[%s]", name, key))
			if err != nil {
				return false, err
			}
			result.SetMapIndex(reflect.ValueOf(key), value.Elem())
			hasData = hasData || ptrHasData
		}
	}
	if hasData {
		additionalProperties.Set(result)
	}
	return hasData, nil
}

// bindFormMap binds form (and file) entries of the form `name[key]=value`
// into a generic map[K]V destination. It is reached from bindFormImpl for
// non-bool-keyed maps; the bool-keyed case is handled separately as a
// nullable wrapper.
func bindFormMap(v reflect.Value, form map[string][]string, files map[string][]*multipart.FileHeader, name string) (bool, error) {
	keyType := v.Type().Key()
	valueType := v.Type().Elem()
	result := reflect.MakeMap(v.Type())
	hasData := false
	prefix := name + "["
	seen := map[string]struct{}{}

	process := func(formKey string) error {
		if !strings.HasPrefix(formKey, prefix) {
			return nil
		}
		inner := strings.TrimPrefix(formKey, prefix)
		end := strings.Index(inner, "]")
		if end < 0 {
			return nil
		}
		innerKey := inner[:end]
		if _, ok := seen[innerKey]; ok {
			return nil
		}
		seen[innerKey] = struct{}{}

		keyPtr := reflect.New(keyType)
		if err := BindStringToObject(innerKey, keyPtr.Interface()); err != nil {
			return err
		}
		valuePtr := reflect.New(valueType)
		innerHasData, err := bindFormImpl(valuePtr.Elem(), form, files, fmt.Sprintf("%s[%s]", name, innerKey))
		if err != nil {
			return err
		}
		if innerHasData {
			result.SetMapIndex(keyPtr.Elem(), valuePtr.Elem())
			hasData = true
		}
		return nil
	}

	for k := range form {
		if err := process(k); err != nil {
			return false, err
		}
	}
	for k := range files {
		if err := process(k); err != nil {
			return false, err
		}
	}

	if hasData {
		v.Set(result)
	}
	return hasData, nil
}

func marshalFormImpl(v reflect.Value, result url.Values, name string) {
	switch v.Kind() {
	case reflect.Pointer:
		if v.IsNil() {
			break
		}
		fallthrough
	case reflect.Interface:
		marshalFormImpl(v.Elem(), result, name)
	case reflect.Slice:
		for i := 0; i < v.Len(); i++ {
			elem := v.Index(i)
			marshalFormImpl(elem, result, fmt.Sprintf("%s[%v]", name, i))
		}
	case reflect.Struct:
		for i := 0; i < v.NumField(); i++ {
			field := v.Type().Field(i)
			tag := formFieldTag(field)
			if field.Name == "AdditionalProperties" && tag == "-" {
				iter := v.MapRange()
				for iter.Next() {
					marshalFormImpl(iter.Value(), result, fmt.Sprintf("%s[%s]", name, iter.Key().String()))
				}
				continue
			}
			if !v.Field(i).CanInterface() || tag == "-" {
				continue
			}
			tag = strings.Split(tag, ",")[0] // extract the name of the tag
			marshalFormImpl(v.Field(i), result, fmt.Sprintf("%s[%s]", name, tag))
		}
	default:
		result[name] = append(result[name], fmt.Sprint(v.Interface()))
	}
}
