package githttp

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path"
	"strings"
)

type GitHttp struct {
	// Root directory to serve repos from
	ProjectRoot string

	// Path to git binary
	GitBinPath string

	// Access rules
	UploadPack  bool
	ReceivePack bool

	// Event handling functions
	EventHandler func(ev Event)
}

// Implement the http.Handler interface
func (g *GitHttp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	g.requestHandler(w, r)
	return
}

// Shorthand constructor for most common scenario
func New(root string) *GitHttp {
	return &GitHttp{
		ProjectRoot: root,
		GitBinPath:  "/usr/bin/git",
		UploadPack:  true,
		ReceivePack: true,
	}
}

// Build root directory if doesn't exist
func (g *GitHttp) Init() (*GitHttp, error) {
	if err := os.MkdirAll(g.ProjectRoot, os.ModePerm); err != nil {
		return nil, err
	}
	return g, nil
}

// Publish event if EventHandler is set
func (g *GitHttp) event(e Event) {
	if g.EventHandler != nil {
		g.EventHandler(e)
	} else {
		fmt.Printf("EVENT: %q\n", e)
	}
}

// Actual command handling functions

func (g *GitHttp) serviceRpc(hr HandlerReq) error {
	w, r, rpc, dir := hr.w, hr.r, hr.Rpc, hr.Dir

	access, err := g.hasAccess(r, dir, rpc, true)
	if err != nil {
		return err
	}

	if access == false {
		return &ErrorNoAccess{hr.Dir}
	}

	// Reader that decompresses if necessary
	reader, err := requestReader(r)
	if err != nil {
		return err
	}
	defer reader.Close()

	// Reader that scans for events
	rpcReader := &RpcReader{
		Reader: reader,
		Rpc:    rpc,
	}

	// Set content type
	w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", rpc))

	args := []string{rpc, "--stateless-rpc", "."}
	cmd := exec.Command(g.GitBinPath, args...)
	cmd.Dir = dir
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return err
	}

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return err
	}
	defer stdout.Close()

	err = cmd.Start()
	if err != nil {
		return err
	}

	// Scan's git command's output for errors
	gitReader := &GitReader{
		Reader: stdout,
	}

	// Copy input to git binary
	io.Copy(stdin, rpcReader)
	stdin.Close()

	// Write git binary's output to http response
	io.Copy(w, gitReader)

	// Wait till command has completed
	mainError := cmd.Wait()

	if mainError == nil {
		mainError = gitReader.GitError
	}

	// Fire events
	for _, e := range rpcReader.Events {
		// Set directory to current repo
		e.Dir = dir
		e.Request = hr.r
		e.Error = mainError

		// Fire event
		g.event(e)
	}

	// Because a response was already written,
	// the header cannot be changed
	return nil
}

func (g *GitHttp) getInfoRefs(hr HandlerReq) error {
	w, r, dir := hr.w, hr.r, hr.Dir
	service_name := getServiceType(r)
	access, err := g.hasAccess(r, dir, service_name, false)
	if err != nil {
		return err
	}

	if !access {
		g.updateServerInfo(dir)
		hdrNocache(w)
		return sendFile("text/plain; charset=utf-8", hr)
	}

	args := []string{service_name, "--stateless-rpc", "--advertise-refs", "."}
	refs, err := g.gitCommand(dir, args...)
	if err != nil {
		return err
	}

	hdrNocache(w)
	w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service_name))
	w.WriteHeader(http.StatusOK)
	w.Write(packetWrite("# service=git-" + service_name + "\n"))
	w.Write(packetFlush())
	w.Write(refs)

	return nil
}

func (g *GitHttp) getInfoPacks(hr HandlerReq) error {
	hdrCacheForever(hr.w)
	return sendFile("text/plain; charset=utf-8", hr)
}

func (g *GitHttp) getLooseObject(hr HandlerReq) error {
	hdrCacheForever(hr.w)
	return sendFile("application/x-git-loose-object", hr)
}

func (g *GitHttp) getPackFile(hr HandlerReq) error {
	hdrCacheForever(hr.w)
	return sendFile("application/x-git-packed-objects", hr)
}

func (g *GitHttp) getIdxFile(hr HandlerReq) error {
	hdrCacheForever(hr.w)
	return sendFile("application/x-git-packed-objects-toc", hr)
}

func (g *GitHttp) getTextFile(hr HandlerReq) error {
	hdrNocache(hr.w)
	return sendFile("text/plain", hr)
}

// Logic helping functions

func sendFile(content_type string, hr HandlerReq) error {
	w, r := hr.w, hr.r
	req_file := path.Join(hr.Dir, hr.File)

	f, err := os.Stat(req_file)
	if err != nil {
		return err
	}

	w.Header().Set("Content-Type", content_type)
	w.Header().Set("Content-Length", fmt.Sprintf("%d", f.Size()))
	w.Header().Set("Last-Modified", f.ModTime().Format(http.TimeFormat))
	http.ServeFile(w, r, req_file)

	return nil
}

func (g *GitHttp) getGitDir(file_path string) (string, error) {
	root := g.ProjectRoot

	if root == "" {
		cwd, err := os.Getwd()

		if err != nil {
			return "", err
		}

		root = cwd
	}

	f := path.Join(root, file_path)
	if _, err := os.Stat(f); os.IsNotExist(err) {
		return "", err
	}

	return f, nil
}

func (g *GitHttp) hasAccess(r *http.Request, dir string, rpc string, check_content_type bool) (bool, error) {
	if check_content_type {
		if r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", rpc) {
			return false, nil
		}
	}

	if !(rpc == "upload-pack" || rpc == "receive-pack") {
		return false, nil
	}
	if rpc == "receive-pack" {
		return g.ReceivePack, nil
	}
	if rpc == "upload-pack" {
		return g.UploadPack, nil
	}

	return g.getConfigSetting(rpc, dir)
}

func (g *GitHttp) getConfigSetting(service_name string, dir string) (bool, error) {
	service_name = strings.Replace(service_name, "-", "", -1)
	setting, err := g.getGitConfig("http."+service_name, dir)
	if err != nil {
		return false, nil
	}

	if service_name == "uploadpack" {
		return setting != "false", nil
	}

	return setting == "true", nil
}

func (g *GitHttp) getGitConfig(config_name string, dir string) (string, error) {
	args := []string{"config", config_name}
	out, err := g.gitCommand(dir, args...)
	if err != nil {
		return "", err
	}
	return string(out)[0 : len(out)-1], nil
}

func (g *GitHttp) updateServerInfo(dir string) ([]byte, error) {
	args := []string{"update-server-info"}
	return g.gitCommand(dir, args...)
}

func (g *GitHttp) gitCommand(dir string, args ...string) ([]byte, error) {
	command := exec.Command(g.GitBinPath, args...)
	command.Dir = dir

	return command.Output()
}