package plg_backend_git

import (
	"fmt"
	. "github.com/mickael-kerjean/filestash/server/common"
	"golang.org/x/crypto/ssh"
	"gopkg.in/src-d/go-git.v4"
	"gopkg.in/src-d/go-git.v4/plumbing"
	"gopkg.in/src-d/go-git.v4/plumbing/object"
	"gopkg.in/src-d/go-git.v4/plumbing/transport"
	"gopkg.in/src-d/go-git.v4/plumbing/transport/http"
	sshgit "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"
)

var git_cache AppCache

type Git struct {
	git *GitLib
}

func init() {
	Backend.Register("git", Git{})

	git_cache = NewAppCache()
	git_cache.OnEvict(func(key string, value interface{}) {
		g := value.(*Git)
		g.Close()
	})
}

type GitParams struct {
	repo           string
	username       string
	password       string
	passphrase     string
	commit         string
	branch         string
	authorName     string
	authorEmail    string
	committerName  string
	committerEmail string
	basePath       string
}

func (git Git) Init(params map[string]string, app *App) (IBackend, error) {
	if obj := git_cache.Get(params); obj != nil {
		return obj.(*Git), nil
	}
	g := &Git{
		git: &GitLib{
			params: &GitParams{
				params["repo"],
				params["username"],
				params["password"],
				params["passphrase"],
				params["commit"],
				params["branch"],
				params["authorName"],
				params["authorEmail"],
				params["committerName"],
				params["committerEmail"],
				"",
			},
		},
	}
	p := g.git.params
	if p.branch == "" {
		p.branch = "master"
	}
	if p.commit == "" {
		p.commit = "{action} ({filename}): {path}"
	}
	if p.authorName == "" {
		p.authorName = "Filestash"
	}
	if p.authorEmail == "" {
		p.authorEmail = "https://filestash.app"
	}
	if p.committerName == "" {
		p.committerName = "Filestash"
	}
	if p.committerEmail == "" {
		p.committerEmail = "https://filestash.app"
	}

	hash := GenerateID(params)
	p.basePath = GetAbsolutePath(
		TMP_PATH,
		"git_"+hash,
	) + "/"

	repo, err := g.git.open(p, p.basePath)
	g.git.repo = repo
	if err != nil {
		return g, err
	}
	git_cache.Set(params, g)
	return g, nil
}

func (g Git) LoginForm() Form {
	return Form{
		Elmnts: []FormElement{
			{
				Name:  "type",
				Value: "git",
				Type:  "hidden",
			},
			{
				Name:        "repo",
				Type:        "text",
				Placeholder: "Repository*",
			},
			{
				Name:        "username",
				Type:        "text",
				Placeholder: "Username",
			},
			{
				Name:        "password",
				Type:        "long_password",
				Placeholder: "Password",
			},
			{
				Name:        "advanced",
				Type:        "enable",
				Placeholder: "Advanced",
				Target: []string{
					"git_path", "git_passphrase", "git_commit",
					"git_branch", "git_author_email", "git_author_name",
					"git_committer_email", "git_committer_name",
				},
			},
			{
				Id:          "git_path",
				Name:        "path",
				Type:        "text",
				Placeholder: "Path",
			},
			{
				Id:          "git_passphrase",
				Name:        "passphrase",
				Type:        "text",
				Placeholder: "Passphrase",
			},
			{
				Id:          "git_commit",
				Name:        "commit",
				Type:        "text",
				Placeholder: "Commit Format: default to \"{action}({filename}): {path}\"",
			},
			{
				Id:          "git_branch",
				Name:        "branch",
				Type:        "text",
				Placeholder: "Branch: default to \"master\"",
			},
			{
				Id:          "git_author_email",
				Name:        "author_email",
				Type:        "text",
				Placeholder: "Author email",
			},
			{
				Id:          "git_author_name",
				Name:        "author_name",
				Type:        "text",
				Placeholder: "Author name",
			},
			{
				Id:          "git_committer_email",
				Name:        "committer_email",
				Type:        "text",
				Placeholder: "Committer email",
			},
			{
				Id:          "git_committer_name",
				Name:        "committer_name",
				Type:        "text",
				Placeholder: "Committer name",
			},
		},
	}
}

func (g Git) Ls(path string) ([]os.FileInfo, error) {
	g.git.refresh()
	p, err := g.path(path)
	if err != nil {
		return nil, NewError(err.Error(), 403)
	}
	file, err := SafeOsOpenFile(p, os.O_RDONLY, os.ModePerm)
	if err != nil {
		return nil, err
	}
	return file.Readdir(0)
}

func (g Git) Cat(path string) (io.ReadCloser, error) {
	p, err := g.path(path)
	if err != nil {
		return nil, NewError(err.Error(), 403)
	}
	return SafeOsOpenFile(p, os.O_RDONLY, os.ModePerm)
}

func (g Git) Mkdir(path string) error {
	p, err := g.path(path)
	if err != nil {
		return NewError(err.Error(), 403)
	}
	return SafeOsMkdir(p, os.ModePerm)
}

func (g Git) Rm(path string) error {
	p, err := g.path(path)
	if err != nil {
		return NewError(err.Error(), 403)
	}
	if err = SafeOsRemoveAll(p); err != nil {
		return NewError(err.Error(), 403)
	}
	message := g.git.message("delete", path)
	if err = g.git.save(message); err != nil {
		return NewError(err.Error(), 403)
	}
	return nil
}

func (g Git) Mv(from string, to string) error {
	fpath, err := g.path(from)
	if err != nil {
		return NewError(err.Error(), 403)
	}
	tpath, err := g.path(to)
	if err != nil {
		return NewError(err.Error(), 403)
	}

	if err = SafeOsRename(fpath, tpath); err != nil {
		return NewError(err.Error(), 403)
	}
	message := g.git.message("move", from)
	if err = g.git.save(message); err != nil {
		return NewError(err.Error(), 403)
	}
	return nil
}

func (g Git) Touch(path string) error {
	p, err := g.path(path)
	if err != nil {
		return NewError(err.Error(), 403)
	}
	file, err := SafeOsOpenFile(p, os.O_WRONLY|os.O_CREATE, os.ModePerm)
	if err != nil {
		return NewError(err.Error(), 403)
	}
	file.Close()

	message := g.git.message("create", path)
	if err = g.git.save(message); err != nil {
		return NewError(err.Error(), 403)
	}
	return nil
}

func (g Git) Save(path string, file io.Reader) error {
	p, err := g.path(path)
	if err != nil {
		return NewError(err.Error(), 403)
	}

	fo, err := SafeOsOpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
	if err != nil {
		return err
	}
	io.Copy(fo, file)
	fo.Close()

	message := g.git.message("save", path)
	if err = g.git.save(message); err != nil {
		return NewError(err.Error(), 403)
	}
	return nil
}

func (g Git) Close() error {
	return os.RemoveAll(g.git.params.basePath)
}

func (g Git) path(path string) (string, error) {
	if path == "" {
		return "", ErrNotValid
	}
	basePath := filepath.Join(g.git.params.basePath, path)
	if string(path[len(path)-1]) == "/" {
		basePath += "/"
	}
	if strings.HasPrefix(basePath, g.git.params.basePath) == false {
		return "", ErrNotFound
	}
	return basePath, nil
}

type GitLib struct {
	repo   *git.Repository
	params *GitParams
}

func (g *GitLib) open(params *GitParams, path string) (*git.Repository, error) {
	g.params = params

	if _, err := os.Stat(g.params.basePath); os.IsNotExist(err) {
		auth, err := g.auth()
		if err != nil {
			return nil, err
		}
		g, err := git.PlainClone(path, false, &git.CloneOptions{
			URL:           g.params.repo,
			Depth:         1,
			ReferenceName: plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", g.params.branch)),
			SingleBranch:  true,
			Auth:          auth,
		})
		if err == transport.ErrEmptyRemoteRepository {
			return g, nil
		}
		return g, err
	}
	return git.PlainOpen(g.params.basePath)
}

func (g *GitLib) save(message string) error {
	w, err := g.repo.Worktree()
	if err != nil {
		return NewError(err.Error(), 500)
	}
	_, err = w.Add(".")
	if err != nil {
		return NewError(err.Error(), 500)
	}

	_, err = w.Commit(message, &git.CommitOptions{
		All: true,
		Author: &object.Signature{
			Name:  g.params.authorName,
			Email: g.params.authorEmail,
			When:  time.Now(),
		},
		Committer: &object.Signature{
			Name:  g.params.committerName,
			Email: g.params.committerEmail,
			When:  time.Now(),
		},
	})
	if err != nil {
		return err
	}

	auth, err := g.auth()
	if err != nil {
		return err
	}
	return g.repo.Push(&git.PushOptions{
		Auth: auth,
	})
}

func (g *GitLib) refresh() error {
	w, err := g.repo.Worktree()
	if err != nil {
		return err
	}
	return w.Pull(&git.PullOptions{RemoteName: "origin"})
}

func (g *GitLib) auth() (transport.AuthMethod, error) {
	if strings.HasPrefix(g.params.repo, "http") {
		return &http.BasicAuth{
			Username: g.params.username,
			Password: g.params.password,
		}, nil
	}
	isPrivateKey := func(pass string) bool {
		if len(pass) > 1000 && strings.HasPrefix(pass, "-----") {
			return true
		}
		return false
	}

	if isPrivateKey(g.params.password) {
		signer, err := ssh.ParsePrivateKeyWithPassphrase([]byte(g.params.password), []byte(g.params.passphrase))
		if err != nil {
			return nil, err
		}
		return &sshgit.PublicKeys{
			User:   "git",
			Signer: signer,
			HostKeyCallbackHelper: sshgit.HostKeyCallbackHelper{
				HostKeyCallback: ssh.InsecureIgnoreHostKey(),
			},
		}, nil
	}

	return &sshgit.Password{
		User:     g.params.username,
		Password: g.params.password,
		HostKeyCallbackHelper: sshgit.HostKeyCallbackHelper{
			HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		},
	}, nil
}

func (g *GitLib) message(action string, path string) string {
	message := strings.Replace(g.params.commit, "{action}", "save", -1)
	message = strings.Replace(message, "{filename}", filepath.Base(path), -1)
	message = strings.Replace(message, "{path}", strings.Replace(path, g.params.basePath, "", -1), -1)
	return message
}
