package modulecreate

import (
	"fmt"
	"io/fs"

	"github.com/gobuffalo/genny/v2"
	"github.com/gobuffalo/plush/v4"
	"github.com/iancoleman/strcase"

	"github.com/ignite/cli/v29/ignite/pkg/errors"
	"github.com/ignite/cli/v29/ignite/pkg/gomodulepath"
	"github.com/ignite/cli/v29/ignite/pkg/xast"
	"github.com/ignite/cli/v29/ignite/pkg/xgenny"
	"github.com/ignite/cli/v29/ignite/pkg/xstrings"
	"github.com/ignite/cli/v29/ignite/templates/field/plushhelpers"
	"github.com/ignite/cli/v29/ignite/templates/module"
)

// NewGenerator returns the generator to scaffold a module inside an app.
func NewGenerator(opts *CreateOptions) (*genny.Generator, error) {
	subBase, err := fs.Sub(fsBase, "files/base")
	if err != nil {
		return nil, errors.Errorf("fail to generate sub: %w", err)
	}

	g := genny.New()
	if err := g.OnlyFS(subBase, nil, nil); err != nil {
		return g, err
	}

	appModulePath := gomodulepath.ExtractAppPath(opts.ModulePath)

	ctx := plush.NewContext()
	ctx.Set("moduleName", opts.ModuleName)
	ctx.Set("modulePath", opts.ModulePath)
	ctx.Set("appName", opts.AppName)
	ctx.Set("protoVer", opts.ProtoVer)
	ctx.Set("dependencies", opts.Dependencies)
	ctx.Set("params", opts.Params)
	ctx.Set("configs", opts.Configs)
	ctx.Set("isIBC", opts.IsIBC)
	ctx.Set("apiPath", fmt.Sprintf("/%s/%s/%s", appModulePath, opts.ModuleName, opts.ProtoVer))
	ctx.Set("protoPkgName", module.ProtoPackageName(appModulePath, opts.ModuleName, opts.ProtoVer))
	ctx.Set("protoModulePkgName", module.ProtoModulePackageName(appModulePath, opts.ModuleName, opts.ProtoVer))
	ctx.Set("toVariableName", strcase.ToLowerCamel)

	plushhelpers.ExtendPlushContext(ctx)
	g.Transformer(xgenny.Transformer(ctx))
	g.Transformer(genny.Replace("{{protoDir}}", opts.ProtoDir))
	g.Transformer(genny.Replace("{{appName}}", opts.AppName))
	g.Transformer(genny.Replace("{{moduleName}}", opts.ModuleName))
	g.Transformer(genny.Replace("{{protoVer}}", opts.ProtoVer))

	return g, nil
}

// NewAppModify returns generator with modifications required to register a module in the app.
func NewAppModify(opts *CreateOptions) *genny.Generator {
	g := genny.New()
	g.RunFn(appModify(opts))
	g.RunFn(appConfigModify(opts))
	if opts.IsIBC {
		g.RunFn(appIBCModify(opts))
	}
	return g
}

func appConfigModify(opts *CreateOptions) genny.RunFn {
	return func(r *genny.Runner) error {
		configPath := module.PathAppConfigGo
		fConfig, err := r.Disk.Find(configPath)
		if err != nil {
			return err
		}

		// Import
		content, err := xast.AppendImports(
			fConfig.String(),
			xast.WithNamedImport(
				"_",
				fmt.Sprintf("%[1]v/x/%[2]v/module", opts.ModulePath, opts.ModuleName),
			),
			xast.WithNamedImport(
				fmt.Sprintf("%[1]vmoduletypes", opts.ModuleName),
				fmt.Sprintf("%[1]v/x/%[2]v/types", opts.ModulePath, opts.ModuleName),
			),
		)
		if err != nil {
			return err
		}

		content, err = AddModuleToAppConfig(content, opts.ModuleName)
		if err != nil {
			return err
		}

		// Module dependencies
		for _, dep := range opts.Dependencies {
			// If bank is a dependency, add account permissions to the module
			if dep.Name == "Bank" {
				replacement := fmt.Sprintf(
					"{Account: %[1]vmoduletypes.ModuleName, Permissions: []string{authtypes.Minter, authtypes.Burner, authtypes.Staking}}",
					opts.ModuleName,
				)

				// Keeper definition
				content, err = xast.ModifyGlobalArrayVar(content, "moduleAccPerms", xast.AppendGlobalArrayValue(replacement))
				if err != nil {
					return err
				}

			}
		}

		newFile := genny.NewFileS(configPath, content)

		return r.File(newFile)
	}
}

// app.go modification when creating a module.
func appModify(opts *CreateOptions) genny.RunFn {
	return func(r *genny.Runner) error {
		appPath := module.PathAppGo
		f, err := r.Disk.Find(appPath)
		if err != nil {
			return err
		}

		// Import
		content, err := xast.AppendImports(
			f.String(),
			xast.WithNamedImport(
				fmt.Sprintf("%[1]vmodulekeeper", opts.ModuleName),
				fmt.Sprintf("%[1]v/x/%[2]v/keeper", opts.ModulePath, opts.ModuleName),
			),
		)
		if err != nil {
			return err
		}

		// Keeper declaration
		content, err = xast.ModifyStruct(
			content,
			"App",
			xast.AppendStructValue(
				fmt.Sprintf("%[1]vKeeper", xstrings.Title(opts.ModuleName)),
				fmt.Sprintf("%[1]vmodulekeeper.Keeper", opts.ModuleName),
			),
		)
		if err != nil {
			return err
		}

		// Keeper definition
		content, err = xast.ModifyFunction(
			content,
			"New",
			xast.AppendInsideFuncCall(
				"Inject",
				fmt.Sprintf("\n&app.%[1]vKeeper", xstrings.Title(opts.ModuleName)),
				-1,
			),
		)
		if err != nil {
			return err
		}

		newFile := genny.NewFileS(appPath, content)
		return r.File(newFile)
	}
}
