package api_documentation

import (
	"fmt"
	"reflect"
	"strings"

	"gitlab.com/uafrica/go-utils/errors"
	"gitlab.com/uafrica/go-utils/handler_utils"
)

type NoParams struct{}
type DocPath map[string]DocMethodInfo

type Docs struct {
	Paths      map[string]DocPath `json:"paths"`
	Components DocSchemas         `json:"components"`
}

type DocSchemas struct {
	Schemas map[string]interface{} `json:"schemas"`
}

type DocMethodInfo struct {
	Description string                       `json:"description"`
	Tags        []string                     `json:"tags"`
	RequestBody *DocRequestBody              `json:"requestBody,omitempty"`
	Parameters  []DocParam                   `json:"parameters,omitempty"`
	Responses   *map[string]DocResponseValue `json:"responses,omitempty"`
}

type DocRequestBody struct {
	Description string                 `json:"description"`
	Required    bool                   `json:"required"`
	Content     map[string]interface{} `json:"content"`
}

type DocParam struct {
	Name        string      `json:"name"`
	In          string      `json:"in"`
	Description string      `json:"description"`
	Schema      interface{} `json:"schema"`
}

type DocSchema struct {
	Ref string `json:"$ref"`
}

type DocResponseValue struct {
	Description string                  `json:"description"`
	Content     *map[string]interface{} `json:"content,omitempty"`
}

type DocSchemaResponse struct {
	Type  *string   `json:"type,omitempty"`
	Items DocSchema `json:"items"`
}

func GetDocs(endpointHandlers map[string]map[string]interface{}) (Docs, error) {
	docs := Docs{
		Paths:      map[string]DocPath{},
		Components: DocSchemas{Schemas: map[string]interface{}{}},
	}

	var validationError error
	if endpointHandlers, validationError = handler_utils.ValidateAPIEndpoints(endpointHandlers); validationError != nil {
		return Docs{}, validationError
	}

	for path, methods := range endpointHandlers {
		docPath := DocPath{}
		for method, methodHandler := range methods {
			docMethod := DocMethodInfo{}
			if handler, ok := methodHandler.(handler_utils.Handler); !ok {
				docMethod.Description = "Not available"
			} else {
				// purpose
				docMethod.Description = "Not available - see request and response structs"
				docMethod.Tags = []string{path}

				// describe parameters
				docMethod.Parameters = []DocParam{}
				for i := 0; i < handler.RequestParamsType.NumField(); i++ {
					f := handler.RequestParamsType.Field(i)

					name := f.Tag.Get("json")
					if name == "" {
						name = f.Name
					}
					name = strings.Replace(name, ",omitempty", "", -1)

					schema, err := StructSchema(f.Type)
					if err != nil {
						return Docs{}, err
					}

					parameter := DocParam{
						Name:        name,
						In:          "query",
						Description: f.Tag.Get("doc"),
						Schema:      schema,
					}

					docMethod.Parameters = append(docMethod.Parameters, parameter)
				}

				// Request
				if handler.RequestBodyType != nil {
					body := reflect.New(handler.RequestBodyType).Interface()
					bodyTypeString := getType(body)

					docMethod.RequestBody = &DocRequestBody{
						Description: "Some description",
						Required:    true,
						Content: map[string]interface{}{
							"application/json": map[string]interface{}{
								"schema": DocSchema{Ref: "#/components/schemas/" + bodyTypeString},
							},
						},
					}

					if handler.RequestBodyType.Kind() == reflect.Struct && handler.RequestBodyType.NumField() > 0 {
						schema, err := StructSchema(handler.RequestBodyType.Field(0).Type)
						if err != nil {
							return Docs{}, err
						}
						docs.Components.Schemas[bodyTypeString] = schema
					}
				}

				// Response
				if handler.ResponseType != nil {
					responses := map[string]DocResponseValue{}
					responseBody := reflect.New(handler.ResponseType).Interface()
					responseBodyTypeString := getType(responseBody)

					response := DocResponseValue{
						Description: "Some description",
					}
					responses["200"] = response

					if responseBodyTypeString != "" {
						response.Content = &map[string]interface{}{
							"application/json": map[string]interface{}{
								"schema": DocSchema{Ref: "#/components/schemas/" + responseBodyTypeString},
							},
						}
					}

					docMethod.Responses = &responses

					if handler.ResponseType.Kind() == reflect.Struct && handler.ResponseType.NumField() > 0 {
						schema, err := StructSchema(handler.ResponseType.Field(0).Type)
						if err != nil {
							return Docs{}, err
						}
						docs.Components.Schemas[responseBodyTypeString] = schema
					}
				}
			}

			docPath[strings.ToLower(method)] = docMethod
		}
		docs.Paths[path] = docPath
	}

	return docs, nil
}

func getType(myvar interface{}) string {
	if t := reflect.TypeOf(myvar); t.Kind() == reflect.Ptr {
		return t.Elem().Name()
	} else {
		return t.Name()
	}
}

func StructSchema(t reflect.Type) (interface{}, error) {
	if t == nil {
		return nil, nil
	}
	schema := map[string]interface{}{}

	description := ""

	if t.Kind() == reflect.Ptr {
		// schema["optional"] = true
		t = t.Elem()
	}

	switch t.Kind() {
	case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int,
		reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint:
		schema["type"] = "integer"
		schema["format"] = fmt.Sprintf("%v", t)
	case reflect.Float64, reflect.Float32:
		schema["type"] = "number"
		schema["format"] = fmt.Sprintf("%v", t)
	case reflect.Bool:
		schema["type"] = "boolean"
		schema["format"] = fmt.Sprintf("%v", t)
	case reflect.String:
		schema["type"] = "string"
		schema["format"] = fmt.Sprintf("%v", t)

	case reflect.Interface:
		schema["type"] = "object" // any value...?

	case reflect.Struct:

		if t.String() == "time.Time" {
			schema["type"] = "string"
			schema["format"] = "date"
			return schema, nil
		}

		schema["type"] = "object"
		properties := map[string]interface{}{}

		for i := 0; i < t.NumField(); i++ {
			f := t.Field(i)
			if !f.Anonymous {
				fieldName := f.Tag.Get("json")
				if fieldName == "" {
					fieldName = f.Name
				}
				if fieldName == "-" {
					continue // json does not marshal these
				}
				fieldName = strings.Replace(fieldName, ",omitempty", "", -1)

				var err error
				fieldDesc := f.Tag.Get("doc")
				if fieldDesc == "" {
					fieldDesc = description + "." + fieldName
				}
				properties[fieldName], err = StructSchema(f.Type)
				if err != nil {
					return nil, errors.Wrapf(err, "failed to document %v.%s", t, fieldName)
				}
			}
		}
		schema["properties"] = properties

	case reflect.Map:
		schema["type"] = "object"
		// keySchema, err := DocSchema("key", t.Key())
		// if err != nil {
		// 	return nil, errors.Wrapf(err, "cannot make schema for %v map key", t)
		// }
		// schema["key"] = keySchema
		// // elemSchema, err := DocSchema("items", t.Elem())
		// if err != nil {
		// 	return nil, errors.Wrapf(err, "cannot make schema for %v map elem", t)
		// }
		// schema["items"] = elemSchema

	case reflect.Slice:
		schema["type"] = "array"
		schema["items"] = t.Elem()
		// elemSchema, err := DocSchema("items", t.Elem())
		// if err != nil {
		// 	return nil, errors.Wrapf(err, "cannot make schema for %v slice elem", t)
		// }
		// schema["items"] = elemSchema

	default:
		return nil, errors.Errorf("cannot generate schema for %v kind=%v", t, t.Kind())
	}

	return schema, nil
}