//go:generate go get github.com/jteeuwen/go-bindata
//go:generate go install github.com/jteeuwen/go-bindata/go-bindata
//go:generate go-bindata -pkg assets -ignore .jsbeautifyrc -prefix "assets/embed" -o assets/binary.go assets/embed/...

// Package filemanager provides middleware for managing files in a directory
// when directory path is requested instead of a specific file. Based on browse
// middleware.
package filemanager

import (
	e "errors"
	"net/http"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/hacdias/caddy-filemanager/assets"
	"github.com/hacdias/caddy-filemanager/config"
	"github.com/hacdias/caddy-filemanager/file"
	"github.com/hacdias/caddy-filemanager/page"
	"github.com/mholt/caddy/caddyhttp/httpserver"
)

// FileManager is an http.Handler that can show a file listing when
// directories in the given paths are specified.
type FileManager struct {
	Next    httpserver.Handler
	Configs []config.Config
}

// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	var (
		c    *config.Config
		fi   *file.Info
		code int
		err  error
		user *config.User
	)

	for i := range f.Configs {
		// Checks if this Path should be handled by File Manager.
		if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
			return f.Next.ServeHTTP(w, r)
		}

		c = &f.Configs[i]

		// Checks if the URL matches the Assets URL. Returns the asset if the
		// method is GET and Status Forbidden otherwise.
		if httpserver.Path(r.URL.Path).Matches(c.BaseURL + assets.BaseURL) {
			if r.Method == http.MethodGet {
				return assets.Serve(w, r, c)
			}

			return http.StatusForbidden, nil
		}

		// Obtains the user
		username, _, _ := r.BasicAuth()
		if _, ok := c.Users[username]; ok {
			user = c.Users[username]
		} else {
			user = c.User
		}

		// Checks if the request URL is for the WebDav server
		if strings.HasPrefix(r.URL.Path, c.WebDavURL) {
			// Checks for user permissions relatively to this PATH
			if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.WebDavURL)) {
				return http.StatusForbidden, nil
			}

			switch r.Method {
			case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
				if !user.AllowEdit {
					return http.StatusForbidden, nil
				}
			case "MKCOL", "COPY":
				if !user.AllowNew {
					return http.StatusForbidden, nil
				}
			}

			// Preprocess the PUT request if it's the case
			if r.Method == http.MethodPut {
				_, err = processPUT(w, r, c, user, fi)
				if err != nil {
					return http.StatusInternalServerError, err
				}
			}

			c.Handler.ServeHTTP(w, r)
			return 0, nil
		}

		// Checks if the User is allowed to access this file
		if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.BaseURL)) {
			if r.Method == http.MethodGet {
				return page.PrintErrorHTML(
					w, http.StatusForbidden,
					e.New("You don't have permission to access this page."),
				)
			}

			return http.StatusForbidden, nil
		}

		if r.Method == http.MethodGet {
			// Gets the information of the directory/file
			fi, code, err = file.GetInfo(r.URL, c, user)
			if err != nil {
				if r.Method == http.MethodGet {
					return page.PrintErrorHTML(w, code, err)
				}
				return code, err
			}

			// If it's a dir and the path doesn't end with a trailing slash,
			// redirect the user.
			if fi.IsDir() && !strings.HasSuffix(r.URL.Path, "/") {
				http.Redirect(w, r, c.AddrPath+r.URL.Path+"/", http.StatusTemporaryRedirect)
				return 0, nil
			}

			// Generate anti security token.
			c.GenerateToken()

			if fi.IsDir() {
				if val, ok := r.URL.Query()["download"]; ok && val[0] != "" {
					return fi.DownloadAs(w, val[0])
				}
			}

			if !fi.IsDir() {
				query := r.URL.Query()
				webdav := false

				if val, ok := query["raw"]; ok && val[0] == "true" {
					webdav = true
				}

				if val, ok := query["download"]; ok && val[0] == "true" {
					w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name())
					webdav = true
				}

				if webdav {
					r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1)
					c.Handler.ServeHTTP(w, r)
					return 0, nil
				}
			}

			code, err := fi.ServeHTTP(w, r, c, user)
			if err != nil {
				return page.PrintErrorHTML(w, code, err)
			}
			return code, err
		}

		if r.Method == http.MethodPost {
			// TODO: This anti CSCF measure is not being applied to requests
			// to the WebDav URL namespace. Anyone has ideas?
			if !c.CheckToken(r) {
				return http.StatusForbidden, nil
			}

			// VCS commands.
			if r.Header.Get("Command") != "" {
				if !user.AllowCommands {
					return http.StatusUnauthorized, nil
				}

				return command(w, r, c, user)
			}
		}

		return http.StatusNotImplemented, nil

	}

	return f.Next.ServeHTTP(w, r)
}

// command handles the requests for VCS related commands: git, svn and mercurial
func command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
	command := strings.Split(r.Header.Get("command"), " ")

	// Check if the command is allowed
	mayContinue := false

	for _, cmd := range u.Commands {
		if cmd == command[0] {
			mayContinue = true
		}
	}

	if !mayContinue {
		return http.StatusForbidden, nil
	}

	// Check if the program is talled is installed on the computer
	if _, err := exec.LookPath(command[0]); err != nil {
		return http.StatusNotImplemented, nil
	}

	path := strings.Replace(r.URL.Path, c.BaseURL, c.Scope, 1)
	path = filepath.Clean(path)

	cmd := exec.Command(command[0], command[1:len(command)]...)
	cmd.Dir = path
	output, err := cmd.CombinedOutput()

	if err != nil {
		return http.StatusInternalServerError, err
	}

	p := &page.Page{Info: &page.Info{Data: string(output)}}
	return p.PrintAsJSON(w)
}