package api_documentation

import (
	"fmt"
	"gitlab.com/uafrica/go-utils/logs"
	"go/doc"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"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"`
	Required bool `json:"required,omitempty"`
	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{}, corePath string) (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
	}

	functionDocs := GetStructDocs(corePath)

	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
				functionName := GetFunctionName(handler.FuncValue.Interface())
				docMethod.Description = functionDocs[functionName]
				docMethod.Tags = []string{path}

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

					shouldAddDoc := f.Tag.Get("doc")
					if shouldAddDoc == "-" {
						continue
					}

					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,
					}

					if  f.Tag.Get("doc_required") == "true" {
						parameter.Required = true
					}

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

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

					docMethod.RequestBody = &DocRequestBody{
						Description: functionDocs[bodyTypeString],
						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: "",
					}
					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"] = "float"
	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
}

func GetStructDocs(corePath string) map[string]string {
	docs := map[string]string{}

	walkFunction := func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() {
			return nil
		}

		fset := token.NewFileSet()
		d, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
		if err != nil {
			fmt.Println(err)
			return nil
		}

		for k, f := range d {
			fmt.Println("package", k)
			p := doc.New(f, "./", 2)

			for _, objectTypes := range p.Types {
				doc := strings.ReplaceAll(objectTypes.Doc, objectTypes.Name, "")
				docs[objectTypes.Name] = doc
			}
			//
			// for _, v := range p.Vars {
			// 	fmt.Println("type", v.Names)
			// 	fmt.Println("docs:", v.Doc)
			// }

			for _, function := range p.Funcs {
				doc := strings.ReplaceAll(function.Doc, function.Name, "")
				docs[function.Name] = doc
			}

			for _, n := range p.Notes {
				fmt.Println("body", n[0].Body)
			}
		}
		return nil
	}

	err := filepath.Walk(corePath, walkFunction)
	if err != nil {
		logs.ErrorWithMsg("Failed to upload files to s3", err)
	}

	return docs
}

func GetFunctionName(temp interface{}) string {
	strs := strings.Split((runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name()), ".")
	return strs[len(strs)-1]
}