package repo

import (
	"fmt"
	"path/filepath"
	"strings"

	"sigs.k8s.io/kustomize/cmd/gorepomod/internal/git"
	"sigs.k8s.io/kustomize/cmd/gorepomod/internal/misc"
	"sigs.k8s.io/kustomize/cmd/gorepomod/internal/utils"
)

const (
	dotGitFileName = ".git"
	srcHint        = "/src/"
	goModFile      = "go.mod"
)

// DotGitData holds basic information about a local .git file
type DotGitData struct {
	// srcPath is the absolute path to the local Go src directory.
	// This used to be $GOPATH/src.
	// It's the directory containing git repository clones.
	srcPath string
	// The path below srcPath to a particular repository
	// directory, a directory containing a .git directory.
	// Typically {repoOrg}/{repoUserName}, e.g. sigs.k8s.io/cli-utils
	repoPath string
}

func (dg *DotGitData) SrcPath() string {
	return dg.srcPath
}

func (dg *DotGitData) RepoPath() string {
	return dg.repoPath
}

func (dg *DotGitData) AbsPath() string {
	return filepath.Join(dg.srcPath, dg.repoPath)
}

// NewDotGitDataFromPath wants the incoming path to hold dotGit
// E.g.
//   ~/gopath/src/sigs.k8s.io/kustomize
//   ~/gopath/src/github.com/monopole/gorepomod
func NewDotGitDataFromPath(path string) (*DotGitData, error) {
	if !utils.DirExists(filepath.Join(path, dotGitFileName)) {
		return nil, fmt.Errorf(
			"%q doesn't have a %q file", path, dotGitFileName)
	}
	// This is an attempt to figure out where the user has cloned
	// their repos.  In the old days, it was an import path under
	// $GOPATH/src.  If we cannot guess it, we may need to ask for it,
	// or maybe proceed without knowing it.
	index := strings.Index(path, srcHint)
	if index < 0 {
		return nil, fmt.Errorf(
			"path %q doesn't contain %q", path, srcHint)
	}
	return &DotGitData{
		srcPath:  path[:index+len(srcHint)-1],
		repoPath: path[index+len(srcHint):],
	}, nil
}

// It's a factory factory.
func (dg *DotGitData) NewRepoFactory(
	exclusions []string) (*ManagerFactory, error) {
	modules, err := loadProtoModules(dg.AbsPath(), exclusions)
	if err != nil {
		return nil, err
	}
	err = dg.checkModules(modules)
	if err != nil {
		return nil, err
	}

	runner := git.NewQuiet(dg.AbsPath(), true)
	remoteName, err := runner.DetermineRemoteToUse()
	if err != nil {
		return nil, err
	}

	// Some tags might exist for modules that
	// have been renamed or deleted; ignore those.
	// There might be newer tags locally than remote,
	// so report both.
	localTags, err := runner.LoadLocalTags()
	if err != nil {
		return nil, err
	}
	remoteTags, err := runner.LoadRemoteTags(remoteName)
	if err != nil {
		return nil, err
	}

	return &ManagerFactory{
		dg:               dg,
		modules:          modules,
		remoteName:       remoteName,
		versionMapLocal:  localTags,
		versionMapRemote: remoteTags,
	}, nil
}

func (dg *DotGitData) checkModules(modules []*protoModule) error {
	for _, pm := range modules {

		file := filepath.Join(pm.PathToGoMod(), goModFile)

		// Do the paths make sense?
		if !strings.HasPrefix(pm.FullPath(), dg.RepoPath()) {
			return fmt.Errorf(
				"module %q doesn't start with the repository name %q",
				pm.FullPath(), dg.RepoPath())
		}

		shortName := pm.ShortName(dg.RepoPath())
		if shortName == misc.ModuleAtTop {
			if pm.PathToGoMod() != dg.AbsPath() {
				return fmt.Errorf("in %q, problem with top module", file)
			}
		} else {
			// Do the relative path and short name make sense?
			if !strings.HasSuffix(pm.PathToGoMod(), string(shortName)) {
				return fmt.Errorf(
					"in %q, the module name %q doesn't match the file's pathToGoMod %q",
					file, shortName, pm.PathToGoMod())
			}
		}
	}
	return nil
}
