// Package gomodulepath implements functions for the manipulation of Go module paths.
// Paths are typically defined as a domain name and a path containing the user and
// repository names, e.g. "github.com/username/reponame", but Go also allows other module
// names like "domain.com/name", "name", "namespace/name", or similar variants.
package gomodulepath

import (
	"fmt"
	"go/parser"
	"go/token"
	"path/filepath"
	"regexp"
	"strings"

	"golang.org/x/mod/module"
	"golang.org/x/mod/semver"

	"github.com/ignite/cli/v29/ignite/pkg/errors"
	"github.com/ignite/cli/v29/ignite/pkg/gomodule"
)

// Path represents a Go module's path.
type Path struct {
	// Path is Go module's full path.
	// e.g.: github.com/ignite/cli.
	RawPath string

	// Root is the root directory name of Go module.
	// e.g.: cli for github.com/ignite/cli.
	Root string

	// Package is the default package name for the Go module that can be used
	// to host main functionality of the module.
	// e.g.: cli for github.com/ignite/cli.
	Package string
}

// Parse parses rawpath into a module Path.
func Parse(rawpath string) (Path, error) {
	if err := validateRawPath(rawpath); err != nil {
		return Path{}, err
	}
	rootName := root(rawpath)
	// package name cannot contain "-" so gracefully remove them
	// if they present.
	packageName := stripNonAlphaNumeric(rootName)
	if err := validatePackageName(packageName); err != nil {
		return Path{}, err
	}
	p := Path{
		RawPath: rawpath,
		Root:    rootName,
		Package: packageName,
	}

	return p, nil
}

// ParseAt parses Go module path of an app resides at path.
func ParseAt(path string) (Path, error) {
	parsed, err := gomodule.ParseAt(path)
	if err != nil {
		return Path{}, err
	}
	return Parse(parsed.Module.Mod.Path)
}

// Find search the Go module in the current and parent paths until finding it.
func Find(path string) (parsed Path, appPath string, err error) {
	for len(path) != 0 && path != "." && path != "/" {
		parsed, err = ParseAt(path)
		if errors.Is(err, gomodule.ErrGoModNotFound) {
			path = filepath.Dir(path)
			continue
		}
		return parsed, path, err
	}
	return Path{}, "", errors.Wrap(gomodule.ErrGoModNotFound, "could not locate your app's root dir")
}

// ExtractAppPath extracts the app module path from a Go module path.
func ExtractAppPath(path string) string {
	if path == "" {
		return ""
	}

	items := strings.Split(path, "/")

	// Remove the first path item if it is assumed to be a domain name
	if len(items) > 1 && strings.Contains(items[0], ".") {
		items = items[1:]
	}

	count := len(items)
	if count == 1 {
		// The Go module path is a single name
		return items[0]
	}

	// The last two items in the path define the namespace and app name
	return strings.Join(items[count-2:], "/")
}

func hasDomainNamePrefix(path string) bool {
	if path == "" {
		return false
	}

	// TODO: should we use a regexp instead of the simplistic check ?
	name, _, _ := strings.Cut(path, "/")
	return strings.Contains(name, ".")
}

func validateRawPath(path string) error {
	// A raw path should be either a URI, a single name or a path
	if hasDomainNamePrefix(path) {
		return validateURIPath(path)
	}
	return validateNamePath(path)
}

func validateURIPath(path string) error {
	if err := module.CheckPath(path); err != nil {
		return errors.Errorf("app name is an invalid go module name: %w", err)
	}
	return nil
}

func validateNamePath(path string) error {
	if err := module.CheckImportPath(path); err != nil {
		return errors.Errorf("app name is an invalid go module name: %w", err)
	}
	return nil
}

func validatePackageName(name string) error {
	fset := token.NewFileSet()
	src := fmt.Sprintf("package %s", name)
	if _, err := parser.ParseFile(fset, "", src, parser.PackageClauseOnly); err != nil {
		// parser error is very low level here so let's hide it from the user
		// completely.
		return errors.New("app name is an invalid go package name")
	}
	return nil
}

func root(path string) string {
	sp := strings.Split(path, "/")
	name := sp[len(sp)-1]
	if semver.IsValid(name) { // omit versions.
		name = sp[len(sp)-2]
	}
	return name
}

func stripNonAlphaNumeric(name string) string {
	reg := regexp.MustCompile(`[^a-zA-Z0-9_]+`)
	return strings.ToLower(reg.ReplaceAllString(name, ""))
}
