package openapi

import (
	"fmt"
	"maps"
	"slices"

	"github.com/getkin/kin-openapi/openapi3"
	"github.com/pkg/errors"
)

// From: https://github.com/danielgtaylor/apisprout/blob/master/example.go

func getSchemaExample(schema *openapi3.Schema) (interface{}, bool) {
	if schema.Example != nil {
		return schema.Example, true
	}

	if schema.Default != nil {
		return schema.Default, true
	}

	if len(schema.Enum) > 0 {
		return schema.Enum[0], true
	}
	return nil, false
}

// stringFormatExample returns an example string based on the given format.
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.7.3
func stringFormatExample(format string) string {
	switch format {
	case "date":
		// https://tools.ietf.org/html/rfc3339
		return "2018-07-23"
	case "date-time":
		// This is the date/time of API Sprout's first commit! :-)
		return "2018-07-23T22:58:00-07:00"
	case "time":
		return "22:58:00-07:00"
	case "email":
		return "email@example.com"
	case "hostname":
		// https://tools.ietf.org/html/rfc2606#page-2
		return "example.com"
	case "ipv4":
		// https://tools.ietf.org/html/rfc5737
		return "198.51.100.0"
	case "ipv6":
		// https://tools.ietf.org/html/rfc3849
		return "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
	case "uri":
		return "https://tools.ietf.org/html/rfc3986"
	case "uri-template":
		// https://tools.ietf.org/html/rfc6570
		return "http://example.com/dictionary/{term:1}/{term}"
	case "json-pointer":
		// https://tools.ietf.org/html/rfc6901
		return "#/components/parameters/term"
	case "regex":
		// https://stackoverflow.com/q/3296050/164268
		return "/^1?$|^(11+?)\\1+$/"
	case "uuid":
		// https://www.ietf.org/rfc/rfc4122.txt
		return "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
	case "password":
		return "********"
	case "binary":
		return "sagefuzzertest"
	}
	return ""
}

// excludeFromMode will exclude a schema if the mode is request and the schema
// is read-only
func excludeFromMode(schema *openapi3.Schema) bool {
	if schema == nil {
		return true
	}

	if schema.ReadOnly {
		return true
	}
	return false
}

// isRequired checks whether a key is actually required.
func isRequired(schema *openapi3.Schema, key string) bool {
	return slices.Contains(schema.Required, key)
}

type cachedSchema struct {
	pending bool
	out     interface{}
}

var (
	// ErrRecursive is when a schema is impossible to represent because it infinitely recurses.
	ErrRecursive = errors.New("Recursive schema")

	// ErrNoExample is sent when no example was found for an operation.
	ErrNoExample = errors.New("No example found")
)

func openAPIExample(schema *openapi3.Schema, cache map[*openapi3.Schema]*cachedSchema) (out interface{}, err error) {
	if ex, ok := getSchemaExample(schema); ok {
		return ex, nil
	}

	cached, ok := cache[schema]
	if !ok {
		cached = &cachedSchema{
			pending: true,
		}
		cache[schema] = cached
	} else if cached.pending {
		return nil, ErrRecursive
	} else {
		return cached.out, nil
	}

	defer func() {
		cached.pending = false
		cached.out = out
	}()

	// Handle combining keywords
	if len(schema.OneOf) > 0 {
		var ex interface{}
		var err error

		for _, candidate := range schema.OneOf {
			ex, err = openAPIExample(candidate.Value, cache)
			if err == nil {
				break
			}
		}
		return ex, err
	}
	if len(schema.AnyOf) > 0 {
		var ex interface{}
		var err error

		for _, candidate := range schema.AnyOf {
			ex, err = openAPIExample(candidate.Value, cache)
			if err == nil {
				break
			}
		}
		return ex, err
	}
	if len(schema.AllOf) > 0 {
		example := map[string]interface{}{}

		for _, allOf := range schema.AllOf {
			candidate, err := openAPIExample(allOf.Value, cache)
			if err != nil {
				return nil, err
			}

			value, ok := candidate.(map[string]interface{})
			if !ok {
				return nil, ErrNoExample
			}

			maps.Copy(example, value)
		}
		return example, nil
	}

	switch {
	case schema.Type.Is("boolean"):
		return true, nil
	case schema.Type.Is("number"), schema.Type.Is("integer"):
		value := 0.0

		if schema.Min != nil && *schema.Min > value {
			value = *schema.Min
			if schema.ExclusiveMin {
				if schema.Max != nil {
					// Make the value half way.
					value = (*schema.Min + *schema.Max) / 2.0
				} else {
					value++
				}
			}
		}

		if schema.Max != nil && *schema.Max < value {
			value = *schema.Max
			if schema.ExclusiveMax {
				if schema.Min != nil {
					// Make the value half way.
					value = (*schema.Min + *schema.Max) / 2.0
				} else {
					value--
				}
			}
		}

		if schema.MultipleOf != nil && int(value)%int(*schema.MultipleOf) != 0 {
			value += float64(int(*schema.MultipleOf) - (int(value) % int(*schema.MultipleOf)))
		}

		if schema.Type.Is("integer") {
			return int(value), nil
		}
		return value, nil
	case schema.Type.Is("string"):
		if ex := stringFormatExample(schema.Format); ex != "" {
			return ex, nil
		}
		example := "string"

		for schema.MinLength > uint64(len(example)) {
			example += example
		}

		if schema.MaxLength != nil && *schema.MaxLength < uint64(len(example)) {
			example = example[:*schema.MaxLength]
		}
		return example, nil
	case schema.Type.Is("array"), schema.Items != nil:
		example := []interface{}{}

		if schema.Items != nil && schema.Items.Value != nil {
			ex, err := openAPIExample(schema.Items.Value, cache)
			if err != nil {
				return nil, fmt.Errorf("can't get example for array item: %+v", err)
			}

			example = append(example, ex)

			for uint64(len(example)) < schema.MinItems {
				example = append(example, ex)
			}
		}
		return example, nil
	case schema.Type.Is("object"), len(schema.Properties) > 0:
		example := map[string]interface{}{}

		for k, v := range schema.Properties {
			if excludeFromMode(v.Value) {
				continue
			}

			ex, err := openAPIExample(v.Value, cache)
			if err == ErrRecursive {
				if isRequired(schema, k) {
					return nil, fmt.Errorf("can't get example for '%s': %+v", k, err)
				}
			} else if err != nil {
				return nil, fmt.Errorf("can't get example for '%s': %+v", k, err)
			} else {
				example[k] = ex
			}
		}

		if schema.AdditionalProperties.Has != nil && schema.AdditionalProperties.Schema != nil {
			addl := schema.AdditionalProperties.Schema.Value

			if !excludeFromMode(addl) {
				ex, err := openAPIExample(addl, cache)
				if err == ErrRecursive {
					// We just won't add this if it's recursive.
				} else if err != nil {
					return nil, fmt.Errorf("can't get example for additional properties: %+v", err)
				} else {
					example["additionalPropertyName"] = ex
				}
			}
		}
		return example, nil
	}
	return nil, ErrNoExample
}

// generateExampleFromSchema creates an example structure from an OpenAPI 3 schema
// object, which is an extended subset of JSON Schema.
//
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject
func generateExampleFromSchema(schema *openapi3.Schema) (interface{}, error) {
	return openAPIExample(schema, make(map[*openapi3.Schema]*cachedSchema)) // TODO: Use caching
}

func generateEmptySchemaValue(contentType string) *openapi3.Schema {
	schema := &openapi3.Schema{}
	objectType := &openapi3.Types{"object"}
	stringType := &openapi3.Types{"string"}

	switch contentType {
	case "application/json":
		schema.Type = objectType
		schema.Properties = make(map[string]*openapi3.SchemaRef)
	case "application/xml":
		schema.Type = stringType
		schema.Format = "xml"
		schema.Example = "<?xml version=\"1.0\"?><root/>"
	case "text/plain":
		schema.Type = stringType
	case "application/x-www-form-urlencoded":
		schema.Type = objectType
		schema.Properties = make(map[string]*openapi3.SchemaRef)
	case "multipart/form-data":
		schema.Type = objectType
		schema.Properties = make(map[string]*openapi3.SchemaRef)
	case "application/octet-stream":
	default:
		schema.Type = stringType
		schema.Format = "binary"
	}

	return schema
}
