package generator

import (
	
	
	
	
	
	
	
	
	

	
	

	am 
	
)

const (
	pkgHelpers = "github.com/pancsta/asyncmachine-go/pkg/helpers"
	pkgTesting = "github.com/pancsta/asyncmachine-go/pkg/helpers/testing"
	pkgRpc     = "github.com/pancsta/asyncmachine-go/pkg/rpc"
)

// GenStarterFiles generates all file contents for a starter kit project
// without writing them to disk. Returns a map of relative file path -> content.
func (
	 context.Context,  cli.StarterParams,
) (map[string]string, error) {
	var  error
	if len(.FileContent) == 0 && .File != "" {
		.FileContent,  = os.ReadFile(.File)
		if  != nil {
			return nil,
				fmt.Errorf("reading YAML schema file %s: %w", .File, )
		}
	}
	if len(.FileContent) == 0 {
		return nil, fmt.Errorf("empty schema file content")
	}

	 := .Name
	if  == "" {
		 = "MyMach"
	}
	 = capitalizeFirstLetter()
	 := camelToSnake()
	 := strings.ReplaceAll(, "_", "")

	var  am.Schema
	if  := yaml.Unmarshal(.FileContent, &);  != nil {
		return nil, fmt.Errorf("parsing YAML schema: %w", )
	}
	if len() == 0 {
		return nil, fmt.Errorf("empty YAML schema")
	}

	// state order
	 := stateNames(.FileContent)
	if len() == 0 {
		return nil, fmt.Errorf("no states found in schema")
	}

	var  []string
	for ,  := range  {
		,  := []
		if  && (slices.Contains(.Tags, "args") ||
			slices.Contains(.Tags, "#args")) {
			 = append(, )
		}
	}

	 := .Inherit
	if len() == 0 {
		 = []string{"basic", "disposed", "rpc/statesrc"}
	}

	// Target state for test
	// Generate the same test every time, when Start state isn't a part of
	// the schema
	 := "Start"
	if !slices.Contains(, "basic") &&
		!slices.Contains(, "Start") {
		 = [0]
	}

	// 1. Generate schema file (states/ss_<name>.go)
	var  []string
	for ,  := range  {
		,  := []
		if ! {
			continue
		}
		 := genState(, )
		 = append(, )
	}

	 := cli.SchemaParams{
		States: strings.Join(, ","),
		SchemaParamsCommon: cli.SchemaParamsCommon{
			Name:    ,
			Inherit: ,
			Global:  true, // Always use --global for schema
		},
	}

	,  := NewSchemaGenerator(, )
	if  != nil {
		return nil, fmt.Errorf("creating schema generator: %w", )
	}

	 := make(map[string]string)

	// states/ss_<name>.go
	 := filepath.Join("states", fmt.Sprintf("ss_%s.go", ))
	[] = .Output()

	// 2. Generate <name>.go
	 := .Uri
	if  == "" {
		 = "asyncmachine.dev/mymach"
	}
	 :=  + "/states"

	 := genMainFile(
		, , , , , ,
		.Args,
	)
	 := fmt.Sprintf("%s.go", )
	[] = 

	// 3. Generate handlers.go (if params.Handlers)
	if .Handlers {
		 := genHandlersFile(, , )
		["handlers.go"] = 
	}

	// 4. Generate <name>_test.go
	 := genTestFile(, "h", )
	 := fmt.Sprintf("%s_test.go", )
	[] = 

	// 5. Generate go.mod (if params.Module)
	if .Module {
		["go.mod"] = fmt.Sprintf(
			"module %s\n\ngo 1.26\n\n"+
				"require github.com/pancsta/asyncmachine-go v0.19.2\n",
			,
		)
	}

	return , nil
}

// GenStarterKit writes all starter files to disk.
func ( context.Context,  *cli.StarterParams) error {
	,  := GenStarterFiles(, *)
	if  != nil {
		return 
	}

	 := .Path
	if  == "" {
		 = "."
	}

	 = filepath.Join(, camelToSnake(.Name))

	for ,  := range  {
		 := filepath.Join(, )
		if fileExists() && !.Force {
			return fmt.Errorf(
				"file %s already exists, delete it or use --force", ,
			)
		}

		if  := os.MkdirAll(filepath.Dir(), 0o755);  != nil {
			return fmt.Errorf("creating directory for %s: %w", , )
		}

		if  := os.WriteFile(, []byte(), 0o666);  != nil {
			return fmt.Errorf("writing file %s: %w", , )
		}

		fmt.Printf("Generated %s\n", )
	}

	if .Module {
		 := exec.CommandContext(, "go", "mod", "tidy")
		.Dir = 
		_ = .Run()
	}

	return nil
}

func genMainFile(
	, , ,  string, ,
	 []string,  bool,
) string {
	 := jen.NewFile()
	.ImportAlias(pkgHelpers, "amhelp")
	.ImportAlias(pkgMachine, "am")
	.ImportAlias(pkgStates, "ssam")
	.ImportName(, "states")
	.ImportAlias(pkgRpc, "arpc")
	if  {
		.ImportName("encoding/gob", "gob")
	}

	.Var().Id("isDebug").Op("=").False()

	.Func().Id("init").Params().Block(
		jen.If(jen.Op("!").Id("isDebug")).Block(
			jen.Return(),
		),
		jen.Line(),
		jen.Comment("manual logging"),
		jen.Comment("amhelp.SetEnvLogLevel(am.LogOps)"),
		jen.Comment("os.Setenv(amhelp.EnvAmLogPrint, \"2\")"),
		jen.Line(),
		jen.Comment("am-dbg is required for debugging, go run it"),
		jen.Comment("go run "+
			"github.com/pancsta/asyncmachine-go/tools/cmd/am-dbg@latest"),
		jen.Qual(pkgHelpers, "EnableDebugging").Call(jen.True()),
	)

	.Var().Id("ss").Op("=").Qual(, +"States")

	.Comment("// ///// ///// /////")
	.Line()
	.Comment("// ///// MACHINE")
	.Line()
	.Comment("// ///// ///// /////")
	.Line()

	.Func().Id("New").Params(
		jen.Id("ctx").Qual("context", "Context"),
	).Params(
		jen.Op("*").Id("Handlers"),
		jen.Error(),
	).Block(
		jen.Comment("handlers"),
		jen.Id("handlers").Op(":=").Op("&").Id("Handlers").Values(jen.Dict{
			jen.Id("DisposedHandlers"): jen.Op("&").
				Qual(pkgStates, "DisposedHandlers").Values(),
		}),
		jen.List(jen.Id("mach"), jen.Err()).Op(":=").
			Qual(pkgMachine, "NewCommon").Call(
			jen.Id("ctx"),
			jen.Lit(),
			jen.Qual(, +"Schema"),
			jen.Id("ss").Dot("Names").Call(),
			jen.Id("handlers"),
			jen.Nil(),
			jen.Nil(),
		),
		jen.If(jen.Err().Op("!=").Nil()).Block(
			jen.Return(jen.Nil(), jen.Err()),
		),
		jen.Id("handlers").Dot("Mach").Op("=").Id("mach"),
		jen.Line(),
		jen.Comment("telemetry and logging"),
		jen.Id("mach").Dot("SetGroups").Call(
			jen.Qual(, +"Groups"),
			jen.Qual(, +"States"),
		),
		jen.Comment("mach.SemLogger().SetLevel(am.LogChanges)"),
		jen.Id("mach").Dot("SemLogger").Call().Dot("SetArgsMapper").Call(
			jen.Qual(pkgHelpers, "LogArgsMapper"),
		),
		jen.Qual(pkgHelpers, "MachDebugEnv").Call(jen.Id("mach")),
		func() jen.Code {
			 := jen.Dict{
				jen.Id("AddrDir"): jen.Lit("."),
			}
			if  {
				[jen.Id("Args")] = jen.Id("ArgsRpc")
			}
			return jen.List(jen.Err(), jen.Id("_")).Op("=").
				Qual(pkgRpc, "MachReplEnv").Call(
				jen.Id("mach"),
				jen.Op("&").Qual(pkgRpc, "ReplOpts").Values(),
			)
		}(),

		jen.Line(),
		jen.Return(jen.Id("handlers"), jen.Nil()),
	)

	.Comment("// ///// ///// /////")
	.Line()
	.Comment("// ///// HANDLERS")
	.Line()
	.Comment("// ///// ///// /////")
	.Line()

	.Comment("// see handler.go for state-state handlers")
	.Line()

	.Type().Id("Handlers").Struct(
		jen.Op("*").Qual(pkgMachine, "ExceptionHandler"),
		jen.Op("*").Qual(pkgStates, "DisposedHandlers"),
		jen.Line(),
		jen.Id("Mach").Op("*").Qual(pkgMachine, "Machine"),
	)

	for ,  := range  {
		.Var().Id("_").Op("=").Id("ss").Dot()
		.Func().Params(jen.Id("h").Op("*").Id("Handlers")).
			Id( + "Enter").Params(jen.Id("e").Op("*").
			Qual(pkgMachine, "Event")).Bool().Block(
			jen.Return(jen.True()),
		)
		.Func().Params(jen.Id("h").Op("*").Id("Handlers")).
			Id( + "State").Params(jen.Id("e").Op("*").
			Qual(pkgMachine, "Event")).Block()
		.Func().Params(jen.Id("h").Op("*").Id("Handlers")).
			Id( + "Exit").Params(jen.Id("e").Op("*").
			Qual(pkgMachine, "Event")).Bool().Block(
			jen.Return(jen.True()),
		)
		.Func().Params(jen.Id("h").Op("*").Id("Handlers")).
			Id( + "End").Params(jen.Id("e").Op("*").
			Qual(pkgMachine, "Event")).Block()
	}

	if  {
		.Comment("// ///// ///// /////")
		.Line()
		.Comment("// ///// ARGS")
		.Line()
		.Comment("// ///// ///// /////")
		.Line()

		.Const().Id("APrefix").Op("=").Lit()

		.Comment("// Args is shared pkg args for Any state")
		.Type().Id("Args").Struct(
			jen.Qual(pkgMachine, "ArgsBase").Tag(map[string]string{"json": "-"}),
		)

		.Func().Params(jen.Id("Args")).Id("ArgsPrefix").Params().String().Block(
			jen.Return(jen.Id("APrefix")),
		)

		.Line()
		.Var().Id("ArgsRpc").Op("=").Index().Qual(pkgMachine, "ArgsApi").Values()
		.Line()

		.Func().Id("init").Params().Block(
			jen.For(jen.List(jen.Id("_"), jen.Id("arg")).
				Op(":=").Range().Id("ArgsRpc")).Block(
				jen.Qual("encoding/gob", "Register").Call(jen.Id("arg")),
			),
		)

		.Comment("// A is an args struct common for all state handlers.")
		.Type().Id("A").Struct(
			jen.Id("Args").Tag(map[string]string{"json": "-"}),
			jen.Line(),
			jen.Comment("// Return chan."),
			jen.Id("ReturnCh").Op("chan<-").Index().String(),
		)

		.Func().Params(jen.Id("A")).Id("ArgsState").Params().String().Block(
			jen.Return(jen.Qual(pkgMachine, "StateAny")),
		)

		if len() > 0 {
			.Comment("// ----- per-state typed args")
			.Line()
			for ,  := range  {
				.Type().Id("A"+).Struct(
					jen.Id("Args").Tag(map[string]string{"json": "-"}),
					jen.Line(),
					jen.Comment("// TODO fields for "+),
				)
				.Func().Params(jen.Id("A" + )).Id("ArgsState").
					Params().String().Block(
					jen.Return(jen.Id("ss").Dot()),
				)
			}
		}
	}

	return foldBoolReturnTrue(.GoString())
}

func genHandlersFile( string, ,  []string) string {
	 := jen.NewFile()
	.ImportAlias(pkgMachine, "am")

	if len() == 0 {
		return .GoString()
	}

	for ,  := range  {
		.Var().Id("_").Op("=").Id("ss").Dot()
		.Comment("// state-state negotiation handlers")
		.Line()

		for ,  := range  {
			if  ==  || slices.Contains(, ) {
				continue
			}
			.Func().Params(jen.Id("h").Op("*").Id("Handlers")).
				Id( + ).Params(jen.Id("e").Op("*").
				Qual(pkgMachine, "Event")).Bool().Block(
				jen.Return(jen.True()),
			)
		}

		.Comment("// globals")
		.Line()
		.Func().Params(jen.Id("h").Op("*").Id("Handlers")).
			Id( + "Any").Params(jen.Id("e").Op("*").
			Qual(pkgMachine, "Event")).Bool().Block(
			jen.Return(jen.True()),
		)
		.Func().Params(jen.Id("h").Op("*").Id("Handlers")).
			Id("Any" + ).Params(jen.Id("e").Op("*").
			Qual(pkgMachine, "Event")).Bool().Block(
			jen.Return(jen.True()),
		)
	}

	return foldBoolReturnTrue(.GoString())
}

func foldBoolReturnTrue( string) string {
	return strings.ReplaceAll(
		, "bool {\n\treturn true\n}", "bool { return true }",
	)
}

func genTestFile(, ,  string) string {
	 := jen.NewFile()
	.ImportAlias(pkgTesting, "amhelpt")
	.ImportAlias(pkgHelpers, "amhelp")

	.Func().Id("Test"+).Params(
		jen.Id("t").Op("*").Qual("testing", "T"),
	).Block(
		jen.Id("ctx").Op(":=").Qual("context", "Background").Call(),
		jen.List(jen.Id(), jen.Err()).Op(":=").
			Id("New").Call(jen.Id("ctx")),
		jen.If(jen.Err().Op("!=").Nil()).Block(
			jen.Id("t").Dot("Fatal").Call(jen.Err()),
		),
		jen.Line(),
		jen.Comment("test "+),
		jen.Id("mach").Op(":=").Id().Dot("Mach"),
		jen.Id("mach").Dot("Add1").Call(
			jen.Id("ss").Dot(), jen.Nil(),
		),
		jen.Qual(pkgTesting, "AssertIs1").Call(
			jen.Id("t"), jen.Id("mach"), jen.Id("ss").Dot(),
		),
		jen.Id("mach").Dot("GoAfter").Call(
			jen.Id("ctx"),
			jen.Qual("time", "Second"),
			jen.Func().Params().Block(
				jen.Id("mach").Dot("Remove1").Call(
					jen.Id("ss").Dot(), jen.Nil(),
				),
			),
		),
		jen.Op("<-").Id("mach").Dot("WhenNot1").Call(
			jen.Id("ss").Dot(), jen.Id("ctx"),
		),
		jen.Comment("enable debug with `true` or AM_DBG_ADDR=1"),
		jen.If(jen.Qual(pkgHelpers, "IsDebug").Call()).Block(
			jen.Qual("time", "Sleep").Call(
				jen.Lit(100).Op("*").Qual("time", "Millisecond"),
			),
		),
	)

	return .GoString()
}

func camelToSnake( string) string {
	 := regexp.MustCompile("([a-z0-9])([A-Z])")
	 := .ReplaceAllString(, "${1}_${2}")
	return strings.ToLower()
}

func fileExists( string) bool {
	,  := os.Stat()
	if errors.Is(, os.ErrNotExist) {
		return false
	}
	return !.IsDir()
}