package got

import (
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	. "git.eugeniocarvalho.dev/eugeniucarvalho/apicodegen/common"
	G "github.com/dave/jennifer/jen"
	"github.com/davecgh/go-spew/spew"
)

type Middleware struct {
	Id   string                             `json:"id"`
	Type string                             `json:"type"`
	Fn   func(ctx *MiddlewareContext) error `json:"-"`
}

type MiddlewareContext struct {
	Project    *Project
	Method     *Method
	Statement  *G.Statement
	Middleware *Middleware
	File       *G.File
}

var (
	Middlewares = map[string]*Middleware{
		"post":      GenCreateStmts,
		"put":       GenUpdateStmts,
		"patch":     GenPatchStmts,
		"delete":    GenDeleteStmts,
		"get_one":   GenGetStmtsOne,
		"pipe":      GenPipeStmts,
		"filter":    GenGetFilter,
		"get_list":  GenGetStmtsList,
		"undelete":  GenUndeleteStmts,
		"implement": GenImplement,
	}
	ResourceWG = sync.WaitGroup{}
)

func InitFile(file *G.File, resource *Resource, p *Project) {
	now := time.Now().UnixNano()

	// file.ImportAlias(
	// file.ImportName(
	// 	fmt.Sprintf("%s/%s/models", p.Custom["go.package.repository"].(string), p.Package),
	// 	"models",
	// )

	if _, defined := p.Custom["go.package.repository"]; !defined {
		panic("go.package.repository not defined in project.json")
	}

	file.Var().Defs(
		G.Id(fmt.Sprintf("_%s_spew_%d_ =", resource.Entity, now)).Qual("github.com/davecgh/go-spew/spew", "Dump"),
		G.Id(fmt.Sprintf("_%s_bson_%d_ *", resource.Entity, now)).Qual(BSON, "M"),
		G.Id(fmt.Sprintf("_%s_time_%d_ *", resource.Entity, now)).Qual("time", "Duration"),
		G.Id(fmt.Sprintf("_%s_error_%d_ *", resource.Entity, now)).Qual(API_ERROR, "Error"),
		G.Id(fmt.Sprintf("_%s_sync_%d_ *", resource.Entity, now)).Qual("sync", "WaitGroup"),
		G.Id(fmt.Sprintf("_%s_primitive_%d_ *", resource.Entity, now)).Qual("go.mongodb.org/mongo-driver/bson/primitive", "ObjectID"),
		G.Id(fmt.Sprintf("_%s_models_%d_ *", resource.Entity, now)).Qual(
			fmt.Sprintf("%s/build/%s/models", p.Custom["go.package.repository"].(string), p.Package),
			"Entity",
		),
		G.Id(fmt.Sprintf("_%s_actions_%d_ *", resource.Entity, now)).Qual(
			fmt.Sprintf("%s/build/%s/actions", p.Custom["go.package.repository"].(string), p.Package),
			"Action",
		),
	)

	file.Func().Id("init").Params().BlockFunc(func(s *G.Group) {
		for _, v := range resource.Formats {
			s.Add(G.Id("FormatSelection").Index(G.Lit(resource.ID + "." + v.Id)).Op("=").Lit(v.Fields))
		}
	})
}

func CreateDummy(p *Project, resource *Resource, method *Method) error {
	// Verifica se existe um arquivo na pasta de include.
	// Caso o arquivo não exista um novo arquivo é criado.

	// outputfile := fmt.Sprintf("%s/include/go/api_%s_%s.go", CurrentDirectory, strings.ToLower(resource.ID), strings.ToLower(method.ID))
	outputfile := fmt.Sprintf(
		"../project/include/go/api_%s_%s_gen.go",
		strings.ToLower(resource.ID),
		strings.ToLower(method.ID),
	)

	if _, err := os.Stat(outputfile); os.IsNotExist(err) {

		file := G.NewFile(p.Package)
		file.Comment(method.Description)

		if o, err := json.MarshalIndent(method, "", "    "); err == nil {
			file.Comment(string(o))
		}

		file.Func().Params(
			G.Id("t").Op("*").Id(ResourceStructId(resource)),
		).Id(method.ID).Params(
			G.Id("ctx").Qual(IRIS_CTX, "Context"),
		).Params(
			G.Id("resp").Interface(),
			G.Id("err").Op("*").Qual(API_ERROR, "Error"),
		).Block(G.Return())

		// if err = Write(path, file); err != nil {
		// 	return err
		// }
		return Write(outputfile, file)
	}
	return nil
}

func GenResources(p *Project) (err error) {
	var (
		file *G.File
		path string
	)

	if err = generateActionCommonFile(p); err != nil {
		return
	}

	for _, resource := range p.Resources {

		// Registra os campos retornados nos formatos do metodo list
		// FormatMap = map[string]string{}
		// Cria um novo arquivo para cada recurso
		file = G.NewFile(p.Package)

		InitFile(file, resource, p)

		// Inseri os comentarios de cada recurso no inicio do arquivo
		file.Comment(resource.Description).Line()
		// Define um tipo para registrar os metodos da api
		file.Type().Id(ResourceStructId(resource)).Struct().Line()

		for _, method := range resource.Methods {
			switch method.Template {
			case "", "implement":
				CreateDummy(p, resource, method)
			default:
				GenMethod(p, file, resource, method)
			}
		}

		// file.Var().DefsFunc(func(s *G.Group) {
		// 	values := G.Dict{}

		// 	for k, v := range FormatMap {
		// 		values[G.Lit(k)] = G.Lit(v)
		// 	}

		// 	s.Add(
		// 		G.Id("Format" + resource.Entity).Op("=").Map(G.String()).String().Values(values),
		// 	)
		// })

		file.Func().Params(
			G.Id("t").Op("*").Id(ResourceStructId((resource))),
		).Id("Fields").Params(
			G.Id("filter").Op("*").Qual(API_URL, "Filter"),
		).Params(
			G.Id("err").Op("*").Qual(API_ERROR, "Error"),
		).Block(
			G.Id(`

			if filter.Fields != nil {
				return
			}

			if filter.Format == "full" {
				filter.Fields = nil
				return
			}

			if fields, has := FormatSelection[`).Lit(fmt.Sprintf("%s.", resource.ID)).Id(`+filter.Format]; has {
				filter.Fields = api.MgoFields(fields)
			} else {

				err = errs.InvalidArgument().Details(&errs.Detail{
					Message: `).Qual("fmt", "Sprintf").Id(`("Invalid value for projection '%s'",filter.Format),
				})
			}
			return
			`),
			// G.If(
			// 	// G.List(G.Id("fields"), G.Id("has")).Op(":=").Id("FormatSelection"+resource.Entity).Index(G.Id("filter").Dot("Format")),
			// 	G.List(G.Id("fields"), G.Id("has")).Op(":=").Id("FormatSelection").Index(
			// 		G.Lit(resource.ID+".").Op("+").Id("filter").Dot("Format"),
			// 	),
			// 	G.Id("has"),
			// ).Block(
			// 	G.Id("filter").Dot("Fields").Op("=").Id("api").Dot("MgoFields").Call(G.Id("fields")),
			// ).Else().Block(
			// 	G.Id("err").Op("=").Qual(API_URL, "Error").Call(
			// 		G.Qual(API_URL, "ERR_INVALID_PARAM_VALUE"),
			// 		G.Lit("Invalid value for %s"),
			// 	),
			// 	// .Dot("Add").Call(
			// 	// 	G.Op("&").Qual(API_URL, "ErrDescription").Values(G.Dict{
			// 	// 		G.Id("Message"): G.Lit(""),
			// 	// 	}),
			// 	// ),
			// ),
		)

		// Fim do loop de methods

		// GenCall(f, resource)

		path = fmt.Sprintf("%s/%s/api_%s_gen.go", p.OutPath, p.Package, strings.ToLower(resource.ID))

		if err = Write(path, file); err != nil {
			return err
		}
	}
	// Fim do loop de recursos
	ResourceWG.Add(2)

	go GenIndexApi(p)

	ResourceWG.Wait()
	return nil
}

func GenQueries(p *Project, resourcesIdMap map[string]bool) {
	defer func() { ResourceWG.Done() }()

	if len(p.Queries.Queries) == 0 {
		return
	}

	file := G.NewFile(p.Package)

	queries := func(s *G.Statement) {
		var (
			query string
			found bool
		)

		for key, _ := range resourcesIdMap {
			if query, found = p.Queries.Queries[fmt.Sprintf("go.%s", key)]; !found {
				query = "{}"
			}
			s.Add(G.Qual(API_URL, "RegisterQuery(").Lit(key).Id(",").Lit(query).Id(")").Line())
		}
	}

	file.Id(`

	func init() {`).Do(queries).Id(`}`)

	if err := Write(fmt.Sprintf("%s/%s/%s_gen.go", p.OutPath, p.Package, "queries"), file); err != nil {
		panic(err)
	}
}

func GenIndexApi(p *Project) error {
	defer func() { ResourceWG.Done() }()
	var (
		stmt           *G.Statement
		params         string
		statments      G.Statement
		idString       string
		middlewares    []string
		Index          = G.NewFile(p.Package)
		RequestParams  = G.Id(`RequestParams := `).Qual(CODE_GEN_V2_COMMON, "RequestParams")
		jwt            = G.Id(`JWT := `).Qual(CODE_GEN_V2_AUTHORIZATION, "Handler")
		addJwt         = false
		queryIndexsMap = map[string]bool{}
		callActionId   = "apply"
	)

	statments = append(statments, G.Id(fmt.Sprintf("%s := ", callActionId)).Qual(API_URL, "CallAction"))
	statments = append(statments, RequestParams)

	// statments = append(statments)

	// Inicializa o mapa de filtros da api
	Index.Id(`
		import(
			// "reflect"
			// "runtime"
			// "strings"
			"fmt"
		)
		var (
			filtersApiReference = map[string]*`).Qual(CODE_GEN_V2_COMMON, "ApiFilter").Id(`{}
			FormatSelection = map[string]string{}
			Debug = api.NewDebug()
		)
		func init(){
			var (
				entity *common.ApiFilter
				files []`).Qual("os", "FileInfo").Id(`
				path  = "./filters"
				err   error
			)

			files, _ = `).Qual(CODE_GEN_V2_COMMON, "GetFiles").Id(`(path)

			for _, file := range files {
				if !file.IsDir() {
					entity = &common.ApiFilter{}  
					if err = common.ParseJson(`).Qual("path/filepath", "Join").Id(`(path, file.Name()), entity); err != nil {
						panic(err)
					}
					filtersApiReference[entity.Id] = entity
				}
			}

		}

		type Action struct {
			Name string
			Fn func(context.Context) (interface{}, *`).Qual(API_ERROR, "Error").Id(`)
		}

		func executeAction(ctx context.Context, actions []Action) (resp interface{},err *errs.Error){
			var (
				stopPropagation bool
				// parts []string
				event *api.DebugEvent
			)
			debug, debugActive := ctx.Values().Get("#debug").(*api.DebugTaks);
			
			for _, action := range actions {
				
				if debugActive {
					fmt.Println("\texecuteAction -> ", action.Name)
					event = debug.Event("execute.action", action.Name)
				}

				resp, err = action.Fn(ctx)
		
				if debugActive {
					event.Data = resp
					event.Error = err
				}

				stopPropagation,_ = ctx.Values().GetBool("stop.propagation")
				switch {
				case stopPropagation, err != nil, resp != nil:
					return
				}
			}
			return
		}
	`)

	for rindex, resource := range p.Resources {

		stmt = G.Line().Id(resource.ID).Op(":=").Id(ResourceStructId(resource)).Values().Line()

		for k, method := range resource.Methods {
			// fmt.Println("----------------------------")
			args := []G.Code{
				G.Lit(method.HttpMethod),
				G.Lit(p.GetPath(method)),
				G.Line().Id("Debug.Handler()"),
			}

			middlewares = []string{}

			if len(method.ParametersString) > 0 {

				params = strings.Join(method.ParametersString, ",")
				// fmt.Println(fmt.Sprintf(`%s|RequestParams("%s", UserRequestParams)`, API_URL, params))
				stmt.Add(G.Line().Comment(
					"Lista de parametros a serem validados durante a requisição",
				).Line().Id(fmt.Sprintf(`args%d%d := "%s"`, rindex, k, params)).Line())
				middlewares = append(middlewares, fmt.Sprintf(`RequestParams(args%d%d, UserRequestParams)`, rindex, k))
			}

			middlewares = append(middlewares, p.Middlewares...)
			middlewares = append(middlewares, method.Middlewares...)

			data := map[string]interface{}{
				"ResourceId": resource.ID,
				"MethodId":   method.ID,
			}

			idString = fmt.Sprintf("%s:%s", resource.ID, method.ID)
			queryIndexsMap[idString] = true

			for _, m := range middlewares {

				if strings.Contains(m, "JWT") && !addJwt {
					addJwt = true
					RequestParams.Line().Add(jwt)
				}

				m = ResolveParams(m, data)

				parts := strings.Split(m, "|")
				// Quando parts possui tamanho maior que
				// significa que foi especificado um middleware de outro pacote.
				if len(parts) > 1 {
					args = append(args, G.Line().Id(callActionId).Call(
						G.Id(parts[1]),
						G.Qual(parts[0], parts[1]),
					))
				} else {
					args = append(args, G.Line().Id(callActionId).Call(
						G.Lit(m),
						G.Id(m),
					))
				}
			}

			args = append(args, G.Line().Id(callActionId).Call(
				G.Lit(fmt.Sprintf("%s.%s", resource.ID, method.ID)),
				G.Id(resource.ID).Dot(method.ID),
			))

			if len(method.Postresponse) > 0 {
				args = append(args, G.Line().Id(method.Postresponse[0]))
			}

			stmt.Add(G.Line().Comment(method.Description).Line().Id("app").Dot("Handle").Call(args...).Line())
		}
		statments = append(statments, stmt)
	}

	// Cria a funcao que trata os filtros
	// statments = append(statments, G.Line().Comment("Filter request").Line().Id("app").Dot("Handle").Call(
	// 	G.Lit("GET"),
	// 	G.Lit(p.BasePath+"/filters/{id:string}"),
	// 	// G.Func().Params(G.Id("ctx").Qual(IRIS_CTX, "Context")).Block(),
	// 	G.Id(fmt.Sprintf(`FilterHandle("../api/%s/filters")`, p.Package)),
	// ))

	// Cria a funcao que trata os metodos options
	statments = append(statments, G.Line().Comment("Options request").Line().Id("app").Dot("Options").Call(
		G.Lit("/{url:path}"),
		G.Func().Params(G.Id("ctx").Qual(IRIS_CTX, "Context")).Block(
			G.Id(`ctx.ResponseWriter().Header().Set("Access-Control-Allow-Origin", ctx.GetHeader("Origin"))`),
		),
	))

	statments = append(statments, G.Line().Comment("Debug eventstream").Line().Id("app").Dot("Get").Call(
		G.Lit("/api/v1/debug"),
		G.Id(`apply("debug", Debug.EventStream())`),
	))
	// Cria a funcao que registra as urls da api no arquivo api_index_gen.go
	Index.Func().Id("Register").Params(
		G.Id("app").Op("*").Qual(IRIS, "Application"),
	).Block(
		statments...,
	).Line()

	go GenQueries(p, queryIndexsMap)

	return Write(fmt.Sprintf("%s/%s/api_index_gen.go", p.OutPath, p.Package), Index)
}

func GenMethod(p *Project, f *G.File, r *Resource, method *Method) error {
	f.Comment(method.Description)

	if o, err := json.MarshalIndent(method, "", "    "); err == nil {
		f.Comment(string(o))
	}

	stmt := f.Func().Params(
		// G.Id("t").Op("*").Id(strings.Title(r.ID)),
		G.Id("t").Op("*").Id(ResourceStructId(r)),
	).Id(method.ID).Params(
		G.Id("ctx").Qual(IRIS_CTX, "Context"),
		// G.Id("resp").Op("*").Qual(API_URL, "ApiResponse"),
	).Params(
		G.Id("resp").Interface(),
		G.Id("err").Op("*").Qual(API_ERROR, "Error"),
	)

	generateActionsFiles(p, f, r, method)

	if middle, found := Middlewares[method.Template]; found {
		ctx := &MiddlewareContext{
			Project:    p,
			Method:     method,
			Middleware: middle,
			Statement:  stmt,
			File:       f,
		}

		return middle.Fn(ctx)
	}
	return fmt.Errorf("Method '%s' template not defined!", method.ID)
}

func generateActionCommonFile(p *Project) (err error) {

	path := fmt.Sprintf(
		"%s/include/go/actions/index_gen.go",
		CurrentDirectory,
	)

	if _, fileErr := os.Stat(path); os.IsNotExist(fileErr) {

		file := G.NewFile("actions")
		file.Id(`
		type Action struct {
			ID string
		}
		`).Line()
		err = Write(path, file)
	}
	return
}
func generateActionsFiles(p *Project, f *G.File, r *Resource, method *Method) {

	actions := []Action{}
	if method.Preconditions != nil {
		actions = append(actions, method.Preconditions...)
	}
	if method.BeforeResponse != nil {
		actions = append(actions, method.BeforeResponse...)
	}

	if method.BeforeParseRequest != nil {
		actions = append(actions, method.BeforeParseRequest...)
	}

	for _, action := range actions {

		path := fmt.Sprintf(
			"%s/include/go/actions/%s_gen.go",
			CurrentDirectory,
			action.ID,
			// method.Entity,
			// method.ID,
			// hookId,
		)

		if _, fileErr := os.Stat(path); os.IsNotExist(fileErr) {
			// methodId := fmt.Sprintf("%s%s%s", strings.Title(hookId), method.Entity, strings.Title(method.ID))

			file := G.NewFile("actions")
			context := map[string]interface{}{
				"imports": map[string]string{
					"errs":    "git.eugeniocarvalho.dev/eugeniucarvalho/apicodegen/api/errs",
					"context": "github.com/kataras/iris/v12/context",
				},
				"function":   strings.Title(action.ID),
				"hasContext": action.Context != nil,
			}

			spew.Dump(action.ID)
			spew.Dump(action.Context)

			out, _ := TemplateToString(hookStmtsTmpl, context)
			file.Id(out).Line()
			Write(path, file)
		}
	}

	// for hookId, _ := range method.Hooks {

	// 	// methodId := fmt.Sprintf("%s%s%s", strings.Title(hookId), method.Entity, strings.Title(method.ID))

	// 	path := fmt.Sprintf(
	// 		"%s/include/go/actions/%s_%s_%s_gen.go",
	// 		CurrentDirectory,
	// 		method.Entity,
	// 		method.ID,
	// 		hookId,
	// 	)

	// 	if _, fileErr := os.Stat(path); os.IsNotExist(fileErr) {

	// 		file := G.NewFile(p.Package)

	// 		context := map[string]interface{}{
	// 			"imports": map[string]string{
	// 				// "api":     "git.eugeniocarvalho.dev/eugeniucarvalho/apicodegen/api",
	// 				"errs":    "git.eugeniocarvalho.dev/eugeniucarvalho/apicodegen/api/errs",
	// 				"context": "github.com/kataras/iris/v12/context",
	// 			},
	// 		}
	// 		out, _ := TemplateToString(hookStmtsTmpl, context)
	// 		file.Id(out).Line()
	// 		Write(path, file)
	// 	}
	// }
}

func GenFromGenericModel(p *Project, entity *EntityInfo) {

	var (
		posfix      string
		propertie   *G.Statement
		tproperties G.Statement
		// file        = G.NewFile(p.Package)
		file        = G.NewFile("models")
		cproperties = map[string]*G.Statement{}
		properties  = G.Statement{}
		values      = G.Dict{}
		model       = p.GetSchema(entity.Name)
		entityName  = entity.NewName
		// filename    = "model_" + strings.ToLower(entityName)
		filename = "models/" + CamelToUnder(entityName)
		propName string
	)

	for _, meta := range model.Properties {
		propName = UpFirst(meta.ID)
		propertie = G.Id(propName)
		meta.FillTags(p, propName)

		posfix = ""
		// Registra a relaao entre as entidades
		if meta.Relation {
			posfix = "Ref"
			SR.Add(&Relation{
				Source:     meta.GetType(),
				Target:     model.ID,
				Attr:       strings.Replace(meta.Tags["bson"], ",omitempty", "", 1),
				DB:         model.DB,
				Collection: model.Collection,
				IsArray:    meta.Array,
			})
		}

		if meta.Array {
			propertie.Index()
		}

		propertie.Id(entity.TranslateType(meta.Type) + posfix)

		// propertie.Id(meta.Type + posfix)

		// Adiciona as tags caso sejam definidas
		if meta.Tags != nil {
			propertie.Tag(meta.Tags)
			// if name, ok := meta.Tags["json"]; ok {
			// }
		}

		// Adiciona a crescricao como comentario
		if meta.Description != "" {
			propertie.Comment(meta.Description)
		}

		cproperties[meta.ID] = propertie

		if meta.ID == "ID" {
			values[G.Id("ID")] = G.Qual(BSON_PRIMITIVE, "NewObjectID").Call()
		}

		// Verifica se possui valor padrão
		if meta.Default != nil {
			values[G.Id(meta.ID)] = G.Lit(meta.Default)
		}

		properties = append(properties, propertie)
	}

	if model.Representations != nil {
		for posfix, rep := range model.Representations {
			tproperties = G.Statement{}

			for _, attr := range rep {
				tproperties = append(tproperties, cproperties[attr])
			}

			file.Comment(
				"Representação " + posfix,
			).Line().Type().Id(
				model.ID + posfix,
			).Struct(tproperties...)
		}

	}

	// Cria a entidade normal
	file.Line().Comment(model.Description).Line().Comment("Representação Completa")
	file.Type().Id(entityName).Struct(properties...)

	file.Comment(fmt.Sprintf("Cria uma instancia de %s.", entityName))

	// Cria a função de instanciar um novo elemento com os valores padrão determinados
	file.Func().Id("New" + entityName).Params().Op("*").Id(entityName).Block(
		G.Return(G.Op("&").Id(entityName).Values(values)),
	)

	// Salva o arquivo da entidade
	// if err := f.Save(fmt.Sprintf("%s/%s/%s_gen.go", p.OutPath, p.Package, filename)); err != nil {
	// fmt.Printf("%s/%s/%s_gen.go", p.OutPath, p.Package, filename)
	if err := Write(fmt.Sprintf("%s/%s/%s_gen.go", p.OutPath, p.Package, filename), file); err != nil {
		panic(err)
	}
}

func Write(path string, file *G.File) error {
	// fmt.Println(fmt.Sprintf("Write -> %#v", path))
	return FilePutContents(path, fmt.Sprintf("%#v", file), 0777)
}

func ResourceStructId(resource *Resource) string {
	return strings.Title(resource.ID) + "Resource"
}
