package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"text/template"
)

type Data struct {
	FeatName string   // input full feature (ex: "master/area")
	Parts    []string // split parts ["master","area"]
	Entity   string   // last ("area")
}

const (
	repoBase    = "gitlab.com/mbugroup/lti-api.git"
	modulesBase = repoBase + "/internal/modules"
)

func main() {
	if len(os.Args) < 2 {
		log.Fatal("usage: make gen feat=<feature>  (ex: customer | master/area)")
	}

	feat := os.Args[1]
	parts := strings.Split(feat, "/")
	entity := parts[len(parts)-1]
	pluralEntityKebab := toPlural(toKebab(entity))

	d := Data{
		FeatName: feat,
		Parts:    parts,
		Entity:   entity,
	}

	// daftar template yang mau diproses
	files := []struct {
		TplPath   string
		OutDir    string
		OutSuffix string
		TplName   string
	}{
		{
			TplPath: "tools/templates/entity.tmpl",
			// Centralize entities at internal/entities
			OutDir:    filepath.Join("internal", "entities"),
			OutSuffix: ".go",
			TplName:   "entity",
		},
		{
			TplPath:   "tools/templates/validation.tmpl",
			OutDir:    filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab, "validations"),
			OutSuffix: ".validation.go",
			TplName:   "validation",
		},
		{
			TplPath:   "tools/templates/service.tmpl",
			OutDir:    filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab, "services"),
			OutSuffix: ".service.go",
			TplName:   "service",
		},
		{
			TplPath:   "tools/templates/controller.tmpl",
			OutDir:    filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab, "controllers"),
			OutSuffix: ".controller.go",
			TplName:   "controller",
		},
		{
			TplPath:   "tools/templates/repository.tmpl",
			OutDir:    filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab, "repositories"),
			OutSuffix: ".repository.go",
			TplName:   "repository",
		},
		{
			TplPath:   "tools/templates/dto.tmpl",
			OutDir:    filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab, "dto"),
			OutSuffix: ".dto.go",
			TplName:   "dto",
		},
		{
			TplPath:   "tools/templates/route.tmpl",
			OutDir:    filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab),
			OutSuffix: "",
			TplName:   "route",
		},
		{
			TplPath:   "tools/templates/module.tmpl",
			OutDir:    filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), pluralEntityKebab),
			OutSuffix: "",
			TplName:   "module",
		},
	}

	for _, file := range files {
		// pastikan template ketemu
		if _, err := os.Stat(file.TplPath); err != nil {
			log.Fatalf("template not found at %s: %v", file.TplPath, err)
		}

		// parse template
		tpl := template.Must(
			template.New(file.TplName).
				Funcs(template.FuncMap{
					"Pascal": toPascalCase,
					"Camel":  toCamelCase,
					"Plural": toPlural,
					"Kebab":  toKebab,
				}).
				ParseFiles(file.TplPath),
		)

		// pastikan folder ada
		if err := os.MkdirAll(file.OutDir, 0o755); err != nil {
			log.Fatalf("make dir: %v", err)
		}

		// nama file output
		var outFile string
		switch file.TplName {
		case "route":
			outFile = filepath.Join(file.OutDir, "route.go")
		case "module":
			outFile = filepath.Join(file.OutDir, "module.go")
		default:
			outFile = filepath.Join(file.OutDir, strings.ToLower(d.Entity)+file.OutSuffix)
		}

		// hindari overwrite
		if _, err := os.Stat(outFile); err == nil {
			log.Fatalf("file already exists: %s", outFile)
		}

		f, err := os.Create(outFile)
		if err != nil {
			log.Fatalf("create file: %v", err)
		}
		defer f.Close()

		if err := tpl.ExecuteTemplate(f, file.TplName, d); err != nil {
			log.Fatal(err)
		}

		log.Println("Generated:", outFile)
	}

	updateParentModules(d)
	updateMainRoute(d)
}

func updateMainRoute(d Data) {
	routeFile := "internal/route/route.go"
	content, err := os.ReadFile(routeFile)
	if err != nil {
		log.Printf("skip update route.go: %v", err)
		return
	}

	var importPath string
	var modName string
	var pkgName string
	if len(d.Parts) > 1 {
		top := d.Parts[0]
		importPath = toKebab(top)
		modName = toCamelCase(top)
		pkgName = toPascalCase(top) + "Module"
	} else {
		importPath = toKebab(d.Entity) + "s"
		modName = toCamelCase(d.Entity) + "s"
		pkgName = toPascalCase(d.Entity) + "Module"
	}

	// Inject import
	importLine := fmt.Sprintf("\t%[1]s \"%s/%s\"", modName, modulesBase, importPath)
	if !strings.Contains(string(content), importLine) {
		content = []byte(strings.Replace(string(content),
			"// MODULE IMPORTS",
			importLine+"\n\t// MODULE IMPORTS",
			1))
	}

	// Inject registry
	registryLine := fmt.Sprintf("\t\t%[1]s.%[2]s{},", modName, pkgName)
	if !strings.Contains(string(content), registryLine) {
		content = []byte(strings.Replace(string(content),
			"// MODULE REGISTRY",
			registryLine+"\n\t\t// MODULE REGISTRY",
			1))
	}

	if err := os.WriteFile(routeFile, content, 0644); err != nil {
		log.Fatal(err)
	}
	log.Println("Updated:", routeFile)
}

func updateParentModules(d Data) {
	if len(d.Parts) < 2 {
		return
	}

	fullLen := len(d.Parts)
	for i := 1; i < len(d.Parts); i++ {
		parentParts := d.Parts[:i]
		ensureParentModuleFile(parentParts)

		childParts := d.Parts[:i+1]
		ensureParentRoute(parentParts, childParts, fullLen)
	}
}

func ensureParentModuleFile(parentParts []string) {
	dir := filepath.Join(append([]string{"internal", "modules"}, toKebabParts(parentParts)...)...)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		log.Fatalf("make parent dir: %v", err)
	}

	moduleFile := filepath.Join(dir, "module.go")
	if _, err := os.Stat(moduleFile); err == nil {
		return
	} else if !errors.Is(err, os.ErrNotExist) {
		log.Fatalf("check parent module: %v", err)
	}

	pkgName := toPackageName(parentParts[len(parentParts)-1])
	structName := toPascalCase(parentParts[len(parentParts)-1]) + "Module"

	content := fmt.Sprintf(`package %s

import (
	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"
)

type %s struct{}

func (%s) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
	RegisterRoutes(router, db, validate)
}
`, pkgName, structName, structName)

	if err := os.WriteFile(moduleFile, []byte(content), 0o644); err != nil {
		log.Fatalf("write parent module: %v", err)
	}
	log.Println("Generated:", moduleFile)
}

func ensureParentRoute(parentParts, childParts []string, fullLen int) {
	dir := filepath.Join(append([]string{"internal", "modules"}, toKebabParts(parentParts)...)...)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		log.Fatalf("make parent dir: %v", err)
	}

	routeFile := filepath.Join(dir, "route.go")
	isLeaf := len(childParts) == fullLen
	childName := childParts[len(childParts)-1]
	childAlias := toCamelCase(childName)
	if isLeaf {
		childAlias += "s"
	}
	childStruct := toPascalCase(childName) + "Module"
	childImportParts := toKebabParts(childParts[:len(childParts)-1])
	childLast := childParts[len(childParts)-1]
	if isLeaf {
		childImportParts = append(childImportParts, toKebab(childLast)+"s")
	} else {
		childImportParts = append(childImportParts, toKebab(childLast))
	}
	childImportPath := modulesBase + "/" + strings.Join(childImportParts, "/")

	if _, err := os.Stat(routeFile); errors.Is(err, os.ErrNotExist) {
		pkgName := toPackageName(parentParts[len(parentParts)-1])
		segment := toKebab(parentParts[len(parentParts)-1])
		content := fmt.Sprintf(`package %s

import (
	"%s"

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"

	%s "%s"
	// MODULE IMPORTS
)

func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
	group := router.Group("/%s")

	allModules := []modules.Module{
		%s.%s{},
		// MODULE REGISTRY
	}

	for _, m := range allModules {
		m.RegisterRoutes(group, db, validate)
	}
}
`, pkgName, modulesBase, childAlias, childImportPath, segment, childAlias, childStruct)

		if err := os.WriteFile(routeFile, []byte(content), 0o644); err != nil {
			log.Fatalf("write parent route: %v", err)
		}
		log.Println("Generated:", routeFile)
		return
	} else if err != nil {
		log.Fatalf("check parent route: %v", err)
	}

	content, err := os.ReadFile(routeFile)
	if err != nil {
		log.Fatalf("read parent route: %v", err)
	}

	importLine := fmt.Sprintf("\t%s \"%s\"", childAlias, childImportPath)
	if !strings.Contains(string(content), importLine) {
		content = []byte(strings.Replace(string(content),
			"\t// MODULE IMPORTS",
			importLine+"\n\t// MODULE IMPORTS",
			1))
	}

	registryLine := fmt.Sprintf("\t\t%s.%s{},", childAlias, childStruct)
	if !strings.Contains(string(content), registryLine) {
		content = []byte(strings.Replace(string(content),
			"\t\t// MODULE REGISTRY",
			registryLine+"\n\t\t// MODULE REGISTRY",
			1))
	}

	if err := os.WriteFile(routeFile, content, 0o644); err != nil {
		log.Fatalf("write parent route: %v", err)
	}
	log.Println("Updated:", routeFile)
}

func toPackageName(s string) string {
	return strings.ToLower(toPascalCase(s))
}

func toPascalCase(s string) string {
	sep := func(r rune) bool { return r == '_' || r == '-' || r == ' ' || r == '/' }
	parts := strings.FieldsFunc(s, sep)
	for i, p := range parts {
		if p == "" {
			continue
		}
		parts[i] = strings.ToUpper(p[:1]) + strings.ToLower(p[1:])
	}
	return strings.Join(parts, "")
}

func toCamelCase(s string) string {
	p := toPascalCase(s)
	if p == "" {
		return ""
	}
	return strings.ToLower(p[:1]) + p[1:]
}

// simple pluralizer (cukup untuk kasus umum: tambah 's')
func toPlural(s string) string {
	s = strings.ToLower(s)
	if strings.HasSuffix(s, "y") && len(s) > 1 {
		prev := s[len(s)-2]
		if !(prev == 'a' || prev == 'i' || prev == 'u' || prev == 'e' || prev == 'o') {
			return s[:len(s)-1] + "ies"
		}
	}
	return s + "s"
}

// kebab-case (untuk folder)
func toKebab(s string) string {
	s = strings.ReplaceAll(s, "_", "-")
	var b strings.Builder
	for i, r := range s {
		if r >= 'A' && r <= 'Z' {
			if i > 0 {
				b.WriteByte('-')
			}
			b.WriteRune(r + 32)
		} else {
			b.WriteRune(r)
		}
	}
	out := b.String()
	out = strings.ReplaceAll(out, "--", "-")
	return strings.Trim(out, "-")
}

// join multiple parts jadi kebab path
func toKebabPath(parts []string) string {
	return filepath.Join(toKebabParts(parts)...)
}

func toKebabParts(parts []string) []string {
	var out []string
	for _, p := range parts {
		out = append(out, toKebab(p))
	}
	return out
}

// join multiple parts jadi camelCase path
// func toCamelPath(parts []string) string {
// 	return filepath.Join(toCamelParts(parts)...)
// }

func toCamelParts(parts []string) []string {
	var out []string
	for i, p := range parts {
		if i == 0 {
			// part pertama lower-case semua
			out = append(out, toCamelCase(p))
		} else {
			// part berikutnya PascalCase biar tetap nyambung camel
			out = append(out, toPascalCase(p))
		}
	}
	return out
}
