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

				// Parameters
				docParameters, err := FillParameters(handler)
				if err != nil {
					return Docs{}, err
				}
				docMethod.Parameters = docParameters

				// Request body
				if handler.RequestBodyType != nil {
					bodyTypeString, requestBody := GetRequestBody(handler, functionDocs)
					docMethod.RequestBody = &requestBody

					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 {
					response, responseBodyTypeString := GetResponse(handler)
					docMethod.Responses = &response
					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 FillParameters(handler handler_utils.Handler) ([]DocParam, error) {

	docParameters := []DocParam{}
	for i := 0; i < handler.RequestParamsType.NumField(); i++ {
		structField := handler.RequestParamsType.Field(i)

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

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

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

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

		docParameters = append(docParameters, parameter)
	}

	return docParameters, nil
}

func GetRequestBody(handler handler_utils.Handler, functionDocs map[string]string) (string, DocRequestBody) {
	body := reflect.New(handler.RequestBodyType).Interface()
	bodyTypeString := getType(body)

	requestBody := DocRequestBody{
		Description: functionDocs[bodyTypeString],
		Required:    true,
		Content: map[string]interface{}{
			"application/json": map[string]interface{}{
				"schema": DocSchema{Ref: "#/components/schemas/" + bodyTypeString},
			},
		},
	}
	return bodyTypeString, requestBody
}

func GetResponse(handler handler_utils.Handler) (map[string]DocResponseValue, string) {
	responses := map[string]DocResponseValue{}
	responseBody := reflect.New(handler.ResponseType).Interface()
	responseBodyTypeString := getType(responseBody)

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

	responses["200"] = response

	return responses, responseBodyTypeString
}

func StructFieldName(structField reflect.StructField) string {
	name := structField.Tag.Get("json")
	if name == "" {
		name = structField.Name
	}

	name = strings.Replace(name, ",omitempty", "", -1)
	return name
}

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"
	case reflect.Slice:
		schema["type"] = "array"
		schema["items"] = t.Elem()
	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 _, 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]
}