package gittags

import (
	"encoding/hex"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"regexp"
	"strings"

	"github.com/blang/semver"
	"github.com/pkg/errors"
)

// TODO: All of this should move somewhere reusable.

const (
	tmpDirPrefix = "gittags-"
)

var (
	refNameRegexp = regexp.MustCompile(`^refs/([^/]+)/([^/]+)$`)
)

func MustGitOidFromString(s string) GitOid {
	oid, err := GitOidFromString(s)
	if err != nil {
		panic(err)
	}
	return oid
}

func GitOidFromString(s string) (GitOid, error) {
	var oid [20]byte

	buf, err := hex.DecodeString(s)
	if err != nil {
		return oid, errors.Wrapf(err, "failed to parse Oid (%q)", s)
	}
	if len(buf) != 20 {
		return oid, fmt.Errorf("bad length for Oid (%d): %q", len(oid), s)
	}

	copy(oid[:], buf[:])
	return oid, nil
}

func GetRemoteRefs(remoteURL string) ([]RefInfo, error) {
	repoPath, err := ioutil.TempDir("", tmpDirPrefix)
	if err != nil {
		return nil, errors.Wrap(err, "failed to create temporary directory")
	}
	defer func() {
		if err := os.RemoveAll(repoPath); err != nil {
			fmt.Printf("warning: failed to remove temporary directory: %v", err) // XXX: to stderr
		}
	}()

	cmd := exec.Command("git", "init", "--bare")
	cmd.Dir = repoPath
	if err := cmd.Run(); err != nil {
		return nil, errors.Wrap(err, "failed to initialize repository")
	}

	cmd = exec.Command("git", "remote", "add", "origin", remoteURL)
	cmd.Dir = repoPath
	if err := cmd.Run(); err != nil {
		return nil, errors.Wrap(err, "failed to add remote")
	}

	cmd = exec.Command("git", "ls-remote", "origin")
	cmd.Dir = repoPath
	out, err := cmd.Output()
	if err != nil {
		return nil, errors.Wrap(err, "failed to list remote")
	}

	lines := strings.Split(string(out), "\n")
	return parseRefLines(lines)
}

// parseRefLines converts the output of e.g. `git ls-remote` into a list of refs and the objects that they point at.
// Annotated tags, which are represented by two refs in that list, are merged into a single logical object.
func parseRefLines(lines []string) ([]RefInfo, error) {
	var refs []RefInfo
	// refsByName := make(map[string]RefInfo)

	for _, line := range lines {
		if line != "" { // We get a trailing newline.
			ref, err := parseRef(line)
			if err != nil {
				return nil, errors.Wrap(err, "failed to parse ref")
			}
			refName := ref.Name()
			if tag, ok := ref.(TagInfo); ok {
				if strings.HasSuffix(refName, "^{}") {
					if len(refs) == 0 {
						return nil, fmt.Errorf("unexpected input: found annotation ref as first in list")
					}
					annotatedRef := refs[len(refs)-1]
					if annotatedRef.Name() != refName[:len(refName)-3] {
						// XXX: We are assuming that the tagged-commit object will be listed *immediately after* the
						//      annotated tag object in the output of `git ls-remote`; is that always true?
						return nil, fmt.Errorf("failed to find annotated tag matching ref: %q", refName)
					}
					annotatedTag, ok := annotatedRef.(TagInfo)
					if !ok {
						return nil, fmt.Errorf("oops: expected annotated tag")
					}

					// Replace old entry with combined entry.
					refs = refs[:len(refs)-1]
					ref = annotatedTag.withCommitOid(tag.CommitOid())
				}
			}
			refs = append(refs, ref)
		}
	}

	return refs, nil
}

func parseRef(line string) (RefInfo, error) {
	parts := strings.Fields(line)
	if len(parts) != 2 {
		return nil, fmt.Errorf("line has unexpected format: %q", line)
	}
	oid, err := GitOidFromString(parts[0])
	if err != nil {
		return nil, err
	}

	// if parts[1] == "HEAD" {
	// 	// default head of the remote repository
	// }

	m := refNameRegexp.FindStringSubmatch(parts[1])
	if m != nil {
		switch m[1] { // ref type
		case "tags":
			v, ok, err := ParseReleaseTagName(m[2])
			if err != nil {
				return nil, errors.Wrap(err, "failed to parse name of release tag")
			}
			if ok {
				return &releaseTagInfo{
					commitOid: oid,
					name:      parts[1],
					version:   v,
				}, nil
			}
			// else (v == nil) this is a tag, but not one that we recognize as a release tag
		case "heads":
			// TODO: nothing special implemented for these, yet
		}
	}

	return &refInfo{
		commitOid: oid,
		name:      parts[1],
	}, nil
}

// ParseReleaseTagName returns (version, ok, err); ok is false iff the tag name doesn't match the pattern we expect for
// release tags.
func ParseReleaseTagName(tagName string) (semver.Version, bool, error) {
	var v semver.Version
	var err error

	if strings.HasPrefix(tagName, "v") {
		if strings.HasSuffix(tagName, "^{}") {
			tagName = tagName[:len(tagName)-3]
		}
		v, err = semver.Parse(tagName[1:])
		if err != nil {
			return v, false, errors.Wrapf(err, "failed to parse semver: %q", tagName[1:])
		}
		return v, true, nil
	}
	return v, false, nil
}
