package vcs

import (
	"fmt"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

var bzrDetectURL = regexp.MustCompile("parent branch: (?P<foo>.+)\n")

// NewBzrRepo creates a new instance of BzrRepo. The remote and local directories
// need to be passed in.
func NewBzrRepo(remote, local string) (*BzrRepo, error) {
	ins := depInstalled("bzr")
	if !ins {
		return nil, NewLocalError("bzr is not installed", nil, "")
	}
	ltype, err := DetectVcsFromFS(local)

	// Found a VCS other than Bzr. Need to report an error.
	if err == nil && ltype != Bzr {
		return nil, ErrWrongVCS
	}

	r := &BzrRepo{}
	r.setRemote(remote)
	r.setLocalPath(local)
	r.Logger = Logger

	// With the other VCS we can check if the endpoint locally is different
	// from the one configured internally. But, with Bzr you can't. For example,
	// if you do `bzr branch https://launchpad.net/govcstestbzrrepo` and then
	// use `bzr info` to get the parent branch you'll find it set to
	// http://bazaar.launchpad.net/~mattfarina/govcstestbzrrepo/trunk/. Notice
	// the change from https to http and the path chance.
	// Here we set the remote to be the local one if none is passed in.
	if err == nil && r.CheckLocal() && remote == "" {
		c := exec.Command("bzr", "info")
		c.Dir = local
		c.Env = envForDir(c.Dir)
		out, err := c.CombinedOutput()
		if err != nil {
			return nil, NewLocalError("Unable to retrieve local repo information", err, string(out))
		}
		m := bzrDetectURL.FindStringSubmatch(string(out))

		// If no remote was passed in but one is configured for the locally
		// checked out Bzr repo use that one.
		if m[1] != "" {
			r.setRemote(m[1])
		}
	}

	return r, nil
}

// BzrRepo implements the Repo interface for the Bzr source control.
type BzrRepo struct {
	base
}

// Vcs retrieves the underlying VCS being implemented.
func (s BzrRepo) Vcs() Type {
	return Bzr
}

// Get is used to perform an initial clone of a repository.
func (s *BzrRepo) Get() error {

	basePath := filepath.Dir(filepath.FromSlash(s.LocalPath()))
	if _, err := os.Stat(basePath); os.IsNotExist(err) {
		err = os.MkdirAll(basePath, 0755)
		if err != nil {
			return NewLocalError("Unable to create directory", err, "")
		}
	}

	out, err := s.run("bzr", "branch", "--", s.Remote(), s.LocalPath())
	if err != nil {
		return NewRemoteError("Unable to get repository", err, string(out))
	}

	return nil
}

// Init initializes a bazaar repository at local location.
func (s *BzrRepo) Init() error {
	out, err := s.run("bzr", "init", "--", s.LocalPath())

	// There are some windows cases where bazaar cannot create the parent
	// directory if it does not already exist, to the location it's trying
	// to create the repo. Catch that error and try to handle it.
	if err != nil && s.isUnableToCreateDir(err) {

		basePath := filepath.Dir(filepath.FromSlash(s.LocalPath()))
		if _, err := os.Stat(basePath); os.IsNotExist(err) {
			err = os.MkdirAll(basePath, 0755)
			if err != nil {
				return NewLocalError("Unable to initialize repository", err, "")
			}

			out, err = s.run("bzr", "init", "--", s.LocalPath())
			if err != nil {
				return NewLocalError("Unable to initialize repository", err, string(out))
			}
			return nil
		}

	} else if err != nil {
		return NewLocalError("Unable to initialize repository", err, string(out))
	}

	return nil
}

// Update performs a Bzr pull and update to an existing checkout.
func (s *BzrRepo) Update() error {
	out, err := s.RunFromDir("bzr", "pull")
	if err != nil {
		return NewRemoteError("Unable to update repository", err, string(out))
	}
	out, err = s.RunFromDir("bzr", "update")
	if err != nil {
		return NewRemoteError("Unable to update repository", err, string(out))
	}
	return nil
}

// UpdateVersion sets the version of a package currently checked out via Bzr.
func (s *BzrRepo) UpdateVersion(version string) error {
	out, err := s.RunFromDir("bzr", "update", "-r", version)
	if err != nil {
		return NewLocalError("Unable to update checked out version", err, string(out))
	}
	return nil
}

// Version retrieves the current version.
func (s *BzrRepo) Version() (string, error) {

	out, err := s.RunFromDir("bzr", "revno", "--tree")
	if err != nil {
		return "", NewLocalError("Unable to retrieve checked out version", err, string(out))
	}

	return strings.TrimSpace(string(out)), nil
}

// Current returns the current version-ish. This means:
// * -1 if on the tip of the branch (this is the Bzr value for HEAD)
// * A tag if on a tag
// * Otherwise a revision
func (s *BzrRepo) Current() (string, error) {
	tip, err := s.CommitInfo("-1")
	if err != nil {
		return "", err
	}

	curr, err := s.Version()
	if err != nil {
		return "", err
	}

	if tip.Commit == curr {
		return "-1", nil
	}

	ts, err := s.TagsFromCommit(curr)
	if err != nil {
		return "", err
	}
	if len(ts) > 0 {
		return ts[0], nil
	}

	return curr, nil
}

// Date retrieves the date on the latest commit.
func (s *BzrRepo) Date() (time.Time, error) {
	out, err := s.RunFromDir("bzr", "version-info", "--custom", "--template={date}")
	if err != nil {
		return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
	}
	t, err := time.Parse(longForm, string(out))
	if err != nil {
		return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out))
	}
	return t, nil
}

// CheckLocal verifies the local location is a Bzr repo.
func (s *BzrRepo) CheckLocal() bool {
	if _, err := os.Stat(s.LocalPath() + "/.bzr"); err == nil {
		return true
	}

	return false
}

// Branches returns a list of available branches on the repository.
// In Bazaar (Bzr) clones and branches are the same. A different branch will
// have a different URL location which we cannot detect from the repo. This
// is a little different from other VCS.
func (s *BzrRepo) Branches() ([]string, error) {
	var branches []string
	return branches, nil
}

// Tags returns a list of available tags on the repository.
func (s *BzrRepo) Tags() ([]string, error) {
	out, err := s.RunFromDir("bzr", "tags")
	if err != nil {
		return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
	}
	tags := s.referenceList(string(out), `(?m-s)^(\S+)`)
	return tags, nil
}

// IsReference returns if a string is a reference. A reference can be a
// commit id or tag.
func (s *BzrRepo) IsReference(r string) bool {
	_, err := s.RunFromDir("bzr", "revno", "-r", r)
	return err == nil
}

// IsDirty returns if the checkout has been modified from the checked
// out reference.
func (s *BzrRepo) IsDirty() bool {
	out, err := s.RunFromDir("bzr", "diff")
	return err != nil || len(out) != 0
}

// CommitInfo retrieves metadata about a commit.
func (s *BzrRepo) CommitInfo(id string) (*CommitInfo, error) {
	r := "-r" + id
	out, err := s.RunFromDir("bzr", "log", r, "--log-format=long")
	if err != nil {
		return nil, ErrRevisionUnavailable
	}

	ci := &CommitInfo{}
	lines := strings.Split(string(out), "\n")
	const format = "Mon 2006-01-02 15:04:05 -0700"
	var track int
	var trackOn bool

	// Note, bzr does not appear to use i18m.
	for i, l := range lines {
		if strings.HasPrefix(l, "revno:") {
			ci.Commit = strings.TrimSpace(strings.TrimPrefix(l, "revno:"))
		} else if strings.HasPrefix(l, "committer:") {
			ci.Author = strings.TrimSpace(strings.TrimPrefix(l, "committer:"))
		} else if strings.HasPrefix(l, "timestamp:") {
			ts := strings.TrimSpace(strings.TrimPrefix(l, "timestamp:"))
			ci.Date, err = time.Parse(format, ts)
			if err != nil {
				return nil, NewLocalError("Unable to retrieve commit information", err, string(out))
			}
		} else if strings.TrimSpace(l) == "message:" {
			track = i
			trackOn = true
		} else if trackOn && i > track {
			ci.Message = ci.Message + l
		}
	}
	ci.Message = strings.TrimSpace(ci.Message)

	// Didn't find the revision
	if ci.Author == "" {
		return nil, ErrRevisionUnavailable
	}

	return ci, nil
}

// TagsFromCommit retrieves tags from a commit id.
func (s *BzrRepo) TagsFromCommit(id string) ([]string, error) {
	out, err := s.RunFromDir("bzr", "tags", "-r", id)
	if err != nil {
		return []string{}, NewLocalError("Unable to retrieve tags", err, string(out))
	}

	tags := s.referenceList(string(out), `(?m-s)^(\S+)`)
	return tags, nil
}

// Ping returns if remote location is accessible.
func (s *BzrRepo) Ping() bool {

	// Running bzr info is slow. Many of the projects are on launchpad which
	// has a public 1.0 API we can use.
	u, err := url.Parse(s.Remote())
	if err == nil {
		if u.Host == "launchpad.net" {
			try := strings.TrimPrefix(u.Path, "/")

			// get returns the body and an err. If the status code is not a 200
			// an error is returned. Launchpad returns a 404 for a codebase that
			// does not exist. Otherwise it returns a JSON object describing it.
			_, er := get("https://api.launchpad.net/1.0/" + try)
			return er == nil
		}
	}

	// This is the same command that Go itself uses but it's not fast (or fast
	// enough by my standards). A faster method would be useful.
	_, err = s.run("bzr", "info", "--", s.Remote())
	return err == nil
}

// ExportDir exports the current revision to the passed in directory.
func (s *BzrRepo) ExportDir(dir string) error {
	out, err := s.RunFromDir("bzr", "export", "--", dir)
	s.log(out)
	if err != nil {
		return NewLocalError("Unable to export source", err, string(out))
	}

	return nil
}

// Multi-lingual manner check for the VCS error that it couldn't create directory.
// https://bazaar.launchpad.net/~bzr-pqm/bzr/bzr.dev/files/head:/po/
func (s *BzrRepo) isUnableToCreateDir(err error) bool {
	msg := err.Error()

	if strings.HasPrefix(msg, fmt.Sprintf("Parent directory of %s does not exist.", s.LocalPath())) ||
		strings.HasPrefix(msg, fmt.Sprintf("Nadřazený adresář %s neexistuje.", s.LocalPath())) ||
		strings.HasPrefix(msg, fmt.Sprintf("El directorio padre de %s no existe.", s.LocalPath())) ||
		strings.HasPrefix(msg, fmt.Sprintf("%s の親ディレクトリがありません。", s.LocalPath())) ||
		strings.HasPrefix(msg, fmt.Sprintf("Родительская директория для %s не существует.", s.LocalPath())) {
		return true
	}

	return false
}
