// Package dfgitutil provides Dockerfile-specific utilities for git refs.
package dfgitutil

import (
	"net/url"
	"path"
	"strconv"
	"strings"

	cerrdefs "github.com/containerd/errdefs"
	"github.com/moby/buildkit/util/gitutil"
	"github.com/pkg/errors"
)

// GitRef represents a git ref.
//
// Examples:
//   - "https://github.com/foo/bar.git#baz/qux:quux/quuz" is parsed into:
//     {Remote: "https://github.com/foo/bar.git", ShortName: "bar", Commit:"baz/qux", SubDir: "quux/quuz"}.
type GitRef struct {
	// Remote is the remote repository path.
	Remote string

	// ShortName is the directory name of the repo.
	// e.g., "bar" for "https://github.com/foo/bar.git"
	ShortName string

	// Ref is a commit hash, a tag, or branch name.
	// Ref is optional.
	Ref string

	// Checksum is a commit hash.
	Checksum string

	// SubDir is a directory path inside the repo.
	// SubDir is optional.
	SubDir string

	// IndistinguishableFromLocal is true for a ref that is indistinguishable from a local file path,
	// e.g., "github.com/foo/bar".
	//
	// Deprecated.
	// Instead, use a distinguishable form such as "https://github.com/foo/bar.git".
	//
	// The dockerfile frontend still accepts this form only for build contexts.
	IndistinguishableFromLocal bool

	// UnencryptedTCP is true for a ref that needs an unencrypted TCP connection,
	// e.g., "git://..." and "http://..." .
	//
	// Discouraged, although not deprecated.
	// Instead, consider using an encrypted TCP connection such as "git@github.com/foo/bar.git" or "https://github.com/foo/bar.git".
	UnencryptedTCP bool

	// KeepGitDir is true for URL that controls whether to keep the .git directory.
	KeepGitDir *bool

	// Submodules is true for URL that controls whether to fetch git submodules.
	Submodules *bool

	// MTime controls file modification time policy: "checkout" (default) or "commit".
	MTime string

	// FetchByCommit, when true, trusts Checksum as the commit and skips comparing
	// it against the remote ref. The commit is fetched directly and the ref name
	// (if any) is applied locally.
	FetchByCommit bool
}

// ParseGitRef parses a git ref.
func ParseGitRef(ref string) (*GitRef, bool, error) {
	res := &GitRef{}

	var (
		remote *gitutil.GitURL
		err    error
	)

	if strings.HasPrefix(ref, "./") || strings.HasPrefix(ref, "../") {
		return nil, false, errors.WithStack(cerrdefs.ErrInvalidArgument)
	} else if strings.HasPrefix(ref, "github.com/") {
		res.IndistinguishableFromLocal = true // Deprecated
		u, err := url.Parse(ref)
		if err != nil {
			return nil, false, err
		}
		u.Scheme = "https"
		remote, err = gitutil.FromURL(u)
		if err != nil {
			return nil, false, err
		}
	} else {
		remote, err = gitutil.ParseURL(ref)
		if errors.Is(err, gitutil.ErrUnknownProtocol) {
			return nil, false, err
		}
		if err != nil {
			return nil, false, err
		}

		switch remote.Scheme {
		case gitutil.HTTPProtocol, gitutil.GitProtocol:
			res.UnencryptedTCP = true // Discouraged, but not deprecated
		}

		switch remote.Scheme {
		// An HTTP(S) URL is considered to be a valid git ref only when it has the ".git[...]" suffix.
		case gitutil.HTTPProtocol, gitutil.HTTPSProtocol:
			if !strings.HasSuffix(remote.Path, ".git") {
				return nil, false, errors.WithStack(cerrdefs.ErrInvalidArgument)
			}
		}
	}

	res.Remote = remote.Remote
	if res.IndistinguishableFromLocal {
		_, res.Remote, _ = strings.Cut(res.Remote, "://")
	}
	if remote.Opts != nil {
		res.Ref, res.SubDir = remote.Opts.Ref, remote.Opts.Subdir
	}

	repoSplitBySlash := strings.Split(res.Remote, "/")
	res.ShortName = strings.TrimSuffix(repoSplitBySlash[len(repoSplitBySlash)-1], ".git")

	if err := res.loadQuery(remote.Query); err != nil {
		return nil, true, err
	}

	return res, true, nil
}

func (gf *GitRef) loadQuery(query url.Values) error {
	if len(query) == 0 {
		return nil
	}
	var tag, branch string
	for k, v := range query {
		switch len(v) {
		case 0, 1:
			if len(v) == 0 || v[0] == "" {
				switch k {
				case "submodules", "keep-git-dir", "fetch-by-commit":
					v = nil
				default:
					return errors.Errorf("query %q has no value", k)
				}
			}
			// NOP
		default:
			return errors.Errorf("query %q has multiple values", k)
		}
		switch k {
		case "ref":
			if gf.Ref != "" && gf.Ref != v[0] {
				return errors.Errorf("ref conflicts: %q vs %q", gf.Ref, v[0])
			}
			gf.Ref = v[0]
		case "tag":
			tag = v[0]
		case "branch":
			branch = v[0]
		case "subdir":
			if gf.SubDir != "" && gf.SubDir != v[0] {
				return errors.Errorf("subdir conflicts: %q vs %q", gf.SubDir, v[0])
			}
			gf.SubDir = v[0]
		case "checksum", "commit":
			gf.Checksum = v[0]
		case "keep-git-dir":
			var vv bool
			if len(v) == 0 {
				vv = true
			} else {
				var err error
				vv, err = strconv.ParseBool(v[0])
				if err != nil {
					return errors.Errorf("invalid keep-git-dir value: %q", v[0])
				}
			}
			gf.KeepGitDir = &vv
		case "submodules":
			var vv bool
			if len(v) == 0 {
				vv = true
			} else {
				var err error
				vv, err = strconv.ParseBool(v[0])
				if err != nil {
					return errors.Errorf("invalid submodules value: %q", v[0])
				}
			}
			gf.Submodules = &vv
		case "mtime":
			switch v[0] {
			case "checkout", "commit":
				gf.MTime = v[0]
			default:
				return errors.Errorf("invalid mtime value: %q (must be \"checkout\" or \"commit\")", v[0])
			}
		case "fetch-by-commit":
			var vv bool
			if len(v) == 0 {
				vv = true
			} else {
				var err error
				vv, err = strconv.ParseBool(v[0])
				if err != nil {
					return errors.Errorf("invalid fetch-by-commit value: %q", v[0])
				}
			}
			gf.FetchByCommit = vv
		default:
			return errors.Errorf("unexpected query %q", k)
		}
	}
	if tag != "" {
		const tagPrefix = "refs/tags/"
		if !strings.HasPrefix(tag, tagPrefix) {
			tag = tagPrefix + tag
		}
		if gf.Ref != "" && gf.Ref != tag {
			return errors.Errorf("ref conflicts: %q vs %q", gf.Ref, tag)
		}
		gf.Ref = tag
	}
	if branch != "" {
		if tag != "" {
			// TODO: consider allowing this, when the tag actually exists on the branch
			return errors.New("branch conflicts with tag")
		}
		const branchPrefix = "refs/heads/"
		if !strings.HasPrefix(branch, branchPrefix) {
			branch = branchPrefix + branch
		}
		if gf.Ref != "" && gf.Ref != branch {
			return errors.Errorf("ref conflicts: %q vs %q", gf.Ref, branch)
		}
		gf.Ref = branch
	}
	return nil
}

// FragmentFormat returns a simplified git URL in fragment format.
// If the URL cannot be parsed, the original string is returned with false.
func FragmentFormat(remote string, withSubdir bool) (string, bool) {
	gitRef, _, err := ParseGitRef(remote)
	if err != nil || gitRef == nil {
		return remote, false
	}
	u := gitRef.Remote
	subdir := ""
	if withSubdir {
		subdir = strings.TrimPrefix(path.Join("/", gitRef.SubDir), "/")
	}
	if gitRef.Ref != "" || subdir != "" {
		u += "#" + gitRef.Ref
		if subdir != "" {
			u += ":" + subdir
		}
	}
	return u, true
}
