package binding

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"reflect"
	"strings"

	"github.com/gin-gonic/gin"
)

const (
	invalidRequestBodyMessage      = "invalid request body"
	maxWorkflowBindingRequestBytes = int64(1 << 20)
	maxWorkflowBindingJSONNesting  = 64
)

var (
	errMultipleBindingJSONValues = errors.New("multiple JSON values")
	errDuplicateBindingJSONKey   = errors.New("duplicate JSON object key")
	errBindingJSONTooDeep        = errors.New("JSON nesting exceeds limit")
	errBindingJSONObjectRequired = errors.New("JSON request body must be an object")
)

// decodeWorkflowBindingJSON rejects unknown and duplicate fields instead of
// accepting misspellings as zero values at a mutation boundary.
func decodeWorkflowBindingJSON[T any](ctx *gin.Context) (T, error) {
	var body T
	if ctx == nil || ctx.Request == nil || ctx.Request.Body == nil {
		return body, bindingJSONError(io.EOF)
	}
	ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, maxWorkflowBindingRequestBytes)
	payload, err := io.ReadAll(ctx.Request.Body)
	if err != nil {
		return body, bindingJSONError(err)
	}
	return decodeWorkflowBindingJSONBytes[T](payload)
}

func decodeWorkflowBindingJSONBytes[T any](payload []byte) (T, error) {
	var body T
	allowedFields, err := bindingJSONFields[T]()
	if err != nil {
		return body, bindingJSONError(err)
	}
	if err := validateBindingJSONValue(payload, allowedFields); err != nil {
		return body, bindingJSONError(err)
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&body); err != nil {
		return body, bindingJSONError(err)
	}
	var trailing any
	if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
		if err == nil {
			err = errMultipleBindingJSONValues
		}
		return body, bindingJSONError(err)
	}
	return body, nil
}

func bindingJSONError(err error) error {
	var maxBytesError *http.MaxBytesError
	if errors.As(err, &maxBytesError) {
		return publicError(http.StatusRequestEntityTooLarge, "binding_request_too_large", invalidRequestBodyMessage)
	}
	return publicError(http.StatusBadRequest, "binding_invalid_json", invalidRequestBodyMessage)
}

func validateBindingJSONValue(payload []byte, allowedRootFields map[string]struct{}) error {
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	first, err := decoder.Token()
	if err != nil {
		return err
	}
	if delimiter, ok := first.(json.Delim); !ok || delimiter != '{' {
		return errBindingJSONObjectRequired
	}
	if err := validateBindingJSONToken(decoder, first, 0, allowedRootFields); err != nil {
		return err
	}
	if _, err := decoder.Token(); !errors.Is(err, io.EOF) {
		if err == nil {
			return errMultipleBindingJSONValues
		}
		return err
	}
	return nil
}

func validateBindingJSONToken(decoder *json.Decoder, token json.Token, depth int, allowedRootFields map[string]struct{}) error {
	delimiter, composite := token.(json.Delim)
	if !composite {
		return nil
	}
	if depth >= maxWorkflowBindingJSONNesting {
		return errBindingJSONTooDeep
	}
	switch delimiter {
	case '{':
		keys := make(map[string]struct{})
		for decoder.More() {
			keyToken, err := decoder.Token()
			if err != nil {
				return err
			}
			key, ok := keyToken.(string)
			if !ok {
				return fmt.Errorf("JSON object key is not a string")
			}
			if _, duplicate := keys[key]; duplicate {
				return fmt.Errorf("%w: %q", errDuplicateBindingJSONKey, key)
			}
			if depth == 0 {
				if _, allowed := allowedRootFields[key]; !allowed {
					return fmt.Errorf("unknown JSON field %q", key)
				}
			}
			keys[key] = struct{}{}
			value, err := decoder.Token()
			if err != nil {
				return err
			}
			if err := validateBindingJSONToken(decoder, value, depth+1, allowedRootFields); err != nil {
				return err
			}
		}
		closing, err := decoder.Token()
		if err != nil {
			return err
		}
		if closing != json.Delim('}') {
			return fmt.Errorf("JSON object is not closed")
		}
	case '[':
		for decoder.More() {
			value, err := decoder.Token()
			if err != nil {
				return err
			}
			if err := validateBindingJSONToken(decoder, value, depth+1, allowedRootFields); err != nil {
				return err
			}
		}
		closing, err := decoder.Token()
		if err != nil {
			return err
		}
		if closing != json.Delim(']') {
			return fmt.Errorf("JSON array is not closed")
		}
	default:
		return fmt.Errorf("unexpected JSON delimiter %q", delimiter)
	}
	return nil
}

func bindingJSONFields[T any]() (map[string]struct{}, error) {
	typeOf := reflect.TypeOf((*T)(nil)).Elem()
	for typeOf.Kind() == reflect.Pointer {
		typeOf = typeOf.Elem()
	}
	if typeOf.Kind() != reflect.Struct {
		return nil, fmt.Errorf("Workflow Binding JSON request type %s is not an object", typeOf)
	}
	fields := make(map[string]struct{}, typeOf.NumField())
	for index := 0; index < typeOf.NumField(); index++ {
		field := typeOf.Field(index)
		if field.PkgPath != "" {
			continue
		}
		name := strings.Split(field.Tag.Get("json"), ",")[0]
		if name == "-" {
			continue
		}
		if name == "" {
			name = field.Name
		}
		fields[name] = struct{}{}
	}
	return fields, nil
}
