package api_documentation

import (
	"fmt"
	"reflect"
	"strings"

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

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

type NoParams struct{}

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

type DocPath struct {
	Methods map[string]DocMethod `json:"methods"`
}

type DocMethod struct {
	Description string              `json:"description"`
	Parameters  map[string]DocParam `json:"parameters,omitempty"`
	Request     interface{}         `json:"request,omitempty"`
	Response    interface{}         `json:"response,omitempty"`
}

type DocParam struct {
	Name        string
	Type        string
	Description string
}

func GetDocs(endpointHandlers map[string]map[string]interface{}) (Docs, error) {
	docs := Docs{
		Paths: map[string]DocPath{},
	}
	for path, methods := range endpointHandlers {
		docPath := DocPath{
			Methods: map[string]DocMethod{},
		}
		for method, methodHandler := range methods {
			docMethod := DocMethod{}
			if handler, ok := methodHandler.(handler_utils.Handler); !ok {
				docMethod.Description = "Not available"
			} else {
				//purpose
				docMethod.Description = "Not available - see request and response structs"

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

					name := f.Tag.Get("json")
					if name == "" {
						name = f.Name
					}

					docMethod.Parameters[f.Name] = DocParam{
						Name:        name,
						Type:        fmt.Sprintf("%v", f.Type),
						Description: f.Tag.Get("doc"),
					}
				}

				//describe request schema
				var err error
				docMethod.Request, err = DocSchema(fmt.Sprintf("%s %s %s", method, path, "request"), handler.RequestBodyType)
				if err != nil {
					return Docs{}, errors.Wrapf(err, "failed to document request")
				}
				docMethod.Response, err = DocSchema(fmt.Sprintf("%s %s %s", method, path, "response"), handler.ResponseType)
				if err != nil {
					return Docs{}, errors.Wrapf(err, "failed to document response")
				}
			}

			docPath.Methods[method] = docMethod
		}
		docs.Paths[path] = docPath
	}
	return docs, nil
}

func DocSchema(description string, t reflect.Type) (interface{}, error) {
	if t == nil {
		return nil, nil
	}
	schema := map[string]interface{}{
		"description": 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,
		reflect.Float64, reflect.Float32,
		reflect.Bool,
		reflect.String:
		schema["type"] = fmt.Sprintf("%v", t)

	case reflect.Interface:
		schema["type"] = "interface{}" //any value...?

	case reflect.Struct:
		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 = DocSchema(fieldDesc, 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"] = "map"
		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"
		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
}