package openapi

import (
	"encoding/json"
	"fmt"

	types "github.com/rancher/wrangler/pkg/schemas"
	"github.com/rancher/wrangler/pkg/schemas/definition"
	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
)

var (
	blacklistFields = map[string]bool{
		"kind":       true,
		"apiVersion": true,
		"metadata":   true,
	}
)

func MustGenerate(obj interface{}) *v1beta1.JSONSchemaProps {
	if obj == nil {
		return nil
	}
	result, err := ToOpenAPIFromStruct(obj)
	if err != nil {
		panic(err)
	}
	return result
}

func ToOpenAPIFromStruct(obj interface{}) (*v1beta1.JSONSchemaProps, error) {
	schemas := types.EmptySchemas()
	schema, err := schemas.Import(obj)
	if err != nil {
		return nil, err
	}

	return toOpenAPI(schema.ID, schemas)
}

func toOpenAPI(name string, schemas *types.Schemas) (*v1beta1.JSONSchemaProps, error) {
	schema := schemas.Schema(name)
	if schema == nil {
		return nil, fmt.Errorf("failed to find schema: %s", name)
	}

	newSchema := schema.DeepCopy()
	if newSchema.InternalSchema != nil {
		newSchema = newSchema.InternalSchema.DeepCopy()
	}
	delete(newSchema.ResourceFields, "kind")
	delete(newSchema.ResourceFields, "apiVersion")
	delete(newSchema.ResourceFields, "metadata")

	return schemaToProps(newSchema, schemas, map[string]bool{})
}

func populateField(fieldJSP *v1beta1.JSONSchemaProps, f *types.Field) error {
	fieldJSP.Description = f.Description
	// don't reset this to not nullable
	if f.Nullable {
		fieldJSP.Nullable = f.Nullable
	}
	fieldJSP.MinLength = f.MinLength
	fieldJSP.MaxLength = f.MaxLength

	if f.Type == "string" && len(f.Options) > 0 {
		for _, opt := range append(f.Options, "") {
			bytes, err := json.Marshal(&opt)
			if err != nil {
				return err
			}
			fieldJSP.Enum = append(fieldJSP.Enum, v1beta1.JSON{
				Raw: bytes,
			})
		}
	}

	if len(f.InvalidChars) > 0 {
		fieldJSP.Pattern = fmt.Sprintf("^[^%s]*$", f.InvalidChars)
	}

	if len(f.ValidChars) > 0 {
		fieldJSP.Pattern = fmt.Sprintf("^[%s]*$", f.ValidChars)
	}

	if f.Min != nil {
		fl := float64(*f.Min)
		fieldJSP.Minimum = &fl
	}

	if f.Max != nil {
		fl := float64(*f.Max)
		fieldJSP.Maximum = &fl
	}

	return nil
}

func typeToProps(typeName string, schemas *types.Schemas, inflight map[string]bool) (*v1beta1.JSONSchemaProps, error) {
	t, subType, schema, err := typeAndSchema(typeName, schemas)
	if err != nil {
		return nil, err
	}

	if schema != nil {
		return schemaToProps(schema, schemas, inflight)
	}

	jsp := &v1beta1.JSONSchemaProps{}

	switch t {
	case "map":
		additionalProps, err := typeToProps(subType, schemas, inflight)
		if err != nil {
			return nil, err
		}
		jsp.Type = "object"
		jsp.Nullable = true
		jsp.AdditionalProperties = &v1beta1.JSONSchemaPropsOrBool{
			Schema: additionalProps,
		}
	case "array":
		items, err := typeToProps(subType, schemas, inflight)
		if err != nil {
			return nil, err
		}
		jsp.Type = "array"
		jsp.Nullable = true
		jsp.Items = &v1beta1.JSONSchemaPropsOrArray{
			Schema: items,
		}
	default:
		jsp.Type = t
	}

	return jsp, nil
}

func schemaToProps(schema *types.Schema, schemas *types.Schemas, inflight map[string]bool) (*v1beta1.JSONSchemaProps, error) {
	jsp := &v1beta1.JSONSchemaProps{
		Description: schema.Description,
		Type:        "object",
	}

	if inflight[schema.ID] {
		return jsp, nil
	}

	inflight[schema.ID] = true
	defer delete(inflight, schema.ID)

	jsp.Properties = map[string]v1beta1.JSONSchemaProps{}

	for name, f := range schema.ResourceFields {
		fieldJSP, err := typeToProps(f.Type, schemas, inflight)
		if err != nil {
			return nil, err
		}
		if err := populateField(fieldJSP, &f); err != nil {
			return nil, err
		}
		if f.Required {
			jsp.Required = append(jsp.Required, name)
		}
		jsp.Properties[name] = *fieldJSP
	}

	return jsp, nil
}

func typeAndSchema(typeName string, schemas *types.Schemas) (string, string, *types.Schema, error) {
	if definition.IsReferenceType(typeName) {
		return "string", "", nil, nil
	}

	if definition.IsArrayType(typeName) {
		return "array", definition.SubType(typeName), nil, nil
	}

	if definition.IsMapType(typeName) {
		return "map", definition.SubType(typeName), nil, nil
	}

	switch typeName {
	// TODO: in v1 set the x- header for this
	case "intOrString":
		return "string", "", nil, nil
	case "int":
		return "integer", "", nil, nil
	case "float":
		return "number", "", nil, nil
	case "string":
		return "string", "", nil, nil
	case "date":
		return "string", "", nil, nil
	case "enum":
		return "string", "", nil, nil
	case "base64":
		return "string", "", nil, nil
	case "password":
		return "string", "", nil, nil
	case "hostname":
		return "string", "", nil, nil
	case "boolean":
		return "boolean", "", nil, nil
	case "json":
		return "object", "", nil, nil
	}

	schema := schemas.Schema(typeName)
	if schema == nil {
		return "", "", nil, fmt.Errorf("failed to find schema %s", typeName)
	}
	if schema.InternalSchema != nil {
		return "", "", schema.InternalSchema, nil
	}
	return "", "", schema, nil
}
