package main

import (
	"errors"
	"go/ast"
	"go/constant"
	"go/importer"
	"go/parser"
	"go/token"
	"go/types"
	"io/fs"
	"log"
	"os"
	"strings"

	"gopkg.in/yaml.v3"
)

func main() {
	if _, err := os.Stat("./go.mod"); errors.Is(err, os.ErrNotExist) {
		log.Fatalln("go.mod not found")
	}

	if err := generateYAML("./internal/domain"); err != nil {
		log.Fatalln(err)
	}
}

func generateYAML(root string) error {
	m, err := getErrorCodesFromFolder(root)
	if err != nil {
		return err
	}

	var description strings.Builder
	enum := make([]string, 0, len(m))

	for c, d := range m {
		if d != "" {
			if description.Len() > 0 {
				description.WriteRune('\n')
			}
			description.WriteString("* `")
			description.WriteString(c)
			description.WriteString("` - ")
			description.WriteString(d)
		}
		enum = append(enum, c)
	}

	return yaml.NewEncoder(os.Stdout).Encode(struct {
		Type        string   `yaml:"type"`
		Description string   `yaml:"description"`
		Enum        []string `yaml:"enum"`
	}{
		Type:        "string",
		Description: description.String(),
		Enum:        enum,
	})
}

//nolint:gocyclo
func getErrorCodesFromFolder(root string) (map[string]string, error) {
	fset := token.NewFileSet()

	astDomainPkg, err := parseDomainPackage(fset, root)
	if err != nil {
		return nil, err
	}

	conf := types.Config{Importer: importer.Default()}

	files := make([]*ast.File, 0, len(astDomainPkg.Files))
	for _, f := range astDomainPkg.Files {
		files = append(files, f)
	}

	domainPkg, err := conf.Check(astDomainPkg.Name, fset, files, nil)
	if err != nil {
		return nil, err
	}
	scope := domainPkg.Scope()

	m := make(map[string]string)

	for _, n := range scope.Names() {
		if !strings.HasPrefix(n, "errorCode") {
			continue
		}

		obj := scope.Lookup(n)

		c, ok := obj.(*types.Const)
		if !ok || c.Val().Kind() != constant.String {
			continue
		}

		f := astDomainPkg.Files[fset.File(c.Pos()).Name()]
		position := fset.Position(c.Pos())
		commentMap := ast.NewCommentMap(fset, f, f.Comments)

		var desc string

		for _, decl := range f.Decls {
			declPosition := fset.Position(decl.Pos())
			if declPosition.Filename != position.Filename || declPosition.Line != position.Line {
				continue
			}

			if comments := commentMap.Filter(decl).Comments(); len(comments) > 0 {
				desc = strings.TrimSpace(comments[0].Text())
			}

			break
		}

		if strings.Contains(desc, "errorcode:ignore") {
			continue
		}

		m[constant.StringVal(c.Val())] = desc
	}

	return m, nil
}

func parseDomainPackage(fset *token.FileSet, root string) (*ast.Package, error) {
	pkgs, err := parser.ParseDir(fset, root, func(info fs.FileInfo) bool {
		if info.IsDir() && info.Name() != root {
			return false
		}

		if info.IsDir() || strings.HasSuffix(info.Name(), "_test.go") {
			return false
		}

		return true
	}, parser.AllErrors|parser.ParseComments)
	if err != nil {
		return nil, err
	}

	astDomainPkg, ok := pkgs["domain"]
	if !ok {
		return nil, errors.New("domain pkg not found")
	}

	return astDomainPkg, nil
}
