package http

import (
	"encoding/json"
	"html/template"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	fb "github.com/filebrowser/filebrowser"
)

// Handler returns a function compatible with http.HandleFunc.
func Handler(m *fb.FileBrowser) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		code, err := serve(&fb.Context{
			FileBrowser: m,
			User:        nil,
			File:        nil,
		}, w, r)

		if code >= 400 {
			w.WriteHeader(code)

			txt := http.StatusText(code)
			log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
			w.Write([]byte(txt + "\n"))
		}

		if err != nil {
			log.Print(err)
		}
	})
}

// serve is the main entry point of this HTML application.
func serve(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	// Checks if the URL contains the baseURL and strips it. Otherwise, it just
	// returns a 404 fb.Error because we're not supposed to be here!
	p := strings.TrimPrefix(r.URL.Path, c.BaseURL)

	if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
		return http.StatusNotFound, nil
	}

	r.URL.Path = p

	// Check if this request is made to the service worker. If so,
	// pass it through a template to add the needed variables.
	if r.URL.Path == "/sw.js" {
		return renderFile(c, w, "sw.js")
	}

	// Checks if this request is made to the static assets folder. If so, and
	// if it is a GET request, returns with the asset. Otherwise, returns
	// a status not implemented.
	if matchURL(r.URL.Path, "/static") {
		if r.Method != http.MethodGet {
			return http.StatusNotImplemented, nil
		}

		return staticHandler(c, w, r)
	}

	// Checks if this request is made to the API and directs to the
	// API handler if so.
	if matchURL(r.URL.Path, "/api") {
		r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
		return apiHandler(c, w, r)
	}

	// If it is a request to the preview and a static website generator is
	// active, build the preview.
	if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil {
		r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview")
		return c.StaticGen.Preview(c, w, r)
	}

	if strings.HasPrefix(r.URL.Path, "/share/") {
		r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
		return sharePage(c, w, r)
	}

	// Any other request should show the index.html file.
	w.Header().Set("x-frame-options", "SAMEORIGIN")
	w.Header().Set("x-content-type-options", "nosniff")
	w.Header().Set("x-xss-protection", "1; mode=block")

	return renderFile(c, w, "index.html")
}

// staticHandler handles the static assets path.
func staticHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	if r.URL.Path != "/static/manifest.json" {
		http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
		return 0, nil
	}

	return renderFile(c, w, "static/manifest.json")
}

// apiHandler is the main entry point for the /api endpoint.
func apiHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	if r.URL.Path == "/auth/get" {
		return authHandler(c, w, r)
	}

	if r.URL.Path == "/auth/renew" {
		return renewAuthHandler(c, w, r)
	}

	valid, _ := validateAuth(c, r)
	if !valid {
		return http.StatusForbidden, nil
	}

	c.Router, r.URL.Path = splitURL(r.URL.Path)

	if !c.User.Allowed(r.URL.Path) {
		return http.StatusForbidden, nil
	}

	if c.StaticGen != nil {
		// If we are using the 'magic url' for the settings,
		// we should redirect the request for the acutual path.
		if r.URL.Path == "/settings" {
			r.URL.Path = c.StaticGen.SettingsPath()
		}

		// Executes the Static website generator hook.
		code, err := c.StaticGen.Hook(c, w, r)
		if code != 0 || err != nil {
			return code, err
		}
	}

	if c.Router == "checksum" || c.Router == "download" {
		var err error
		c.File, err = fb.GetInfo(r.URL, c.FileBrowser, c.User)
		if err != nil {
			return ErrorToHTTP(err, false), err
		}
	}

	var code int
	var err error

	switch c.Router {
	case "download":
		code, err = downloadHandler(c, w, r)
	case "checksum":
		code, err = checksumHandler(c, w, r)
	case "command":
		code, err = command(c, w, r)
	case "search":
		code, err = search(c, w, r)
	case "resource":
		code, err = resourceHandler(c, w, r)
	case "users":
		code, err = usersHandler(c, w, r)
	case "settings":
		code, err = settingsHandler(c, w, r)
	case "share":
		code, err = shareHandler(c, w, r)
	default:
		code = http.StatusNotFound
	}

	return code, err
}

// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
func checksumHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	query := r.URL.Query().Get("algo")

	val, err := c.File.Checksum(query)
	if err == fb.ErrInvalidOption {
		return http.StatusBadRequest, err
	} else if err != nil {
		return http.StatusInternalServerError, err
	}

	w.Write([]byte(val))
	return 0, nil
}

// splitURL splits the path and returns everything that stands
// before the first slash and everything that goes after.
func splitURL(path string) (string, string) {
	if path == "" {
		return "", ""
	}

	path = strings.TrimPrefix(path, "/")

	i := strings.Index(path, "/")
	if i == -1 {
		return "", path
	}

	return path[0:i], path[i:]
}

// renderFile renders a file using a template with some needed variables.
func renderFile(c *fb.Context, w http.ResponseWriter, file string) (int, error) {
	tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file)))

	var contentType string
	switch filepath.Ext(file) {
	case ".html":
		contentType = "text/html"
	case ".js":
		contentType = "application/javascript"
	case ".json":
		contentType = "application/json"
	default:
		contentType = "text"
	}

	w.Header().Set("Content-Type", contentType+"; charset=utf-8")

	data := map[string]interface{}{
		"BaseURL":       c.RootURL(),
		"NoAuth":        c.NoAuth,
		"Version":       fb.Version,
		"CSS":           template.CSS(c.CSS),
		"ReCaptcha":     c.ReCaptchaKey != "" && c.ReCaptchaSecret != "",
		"ReCaptchaHost": c.ReCaptchaHost,
		"ReCaptchaKey":  c.ReCaptchaKey,
	}

	if c.StaticGen != nil {
		data["StaticGen"] = c.StaticGen.Name()
	}

	err := tpl.Execute(w, data)

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

	return 0, nil
}

// sharePage build the share page.
func sharePage(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
	s, err := c.Store.Share.Get(r.URL.Path)
	if err == fb.ErrNotExist {
		w.WriteHeader(http.StatusNotFound)
		return renderFile(c, w, "static/share/404.html")
	}

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

	if s.Expires && s.ExpireDate.Before(time.Now()) {
		c.Store.Share.Delete(s.Hash)
		w.WriteHeader(http.StatusNotFound)
		return renderFile(c, w, "static/share/404.html")
	}

	r.URL.Path = s.Path

	info, err := os.Stat(s.Path)
	if err != nil {
		c.Store.Share.Delete(s.Hash)
		return ErrorToHTTP(err, false), err
	}

	c.File = &fb.File{
		Path:    s.Path,
		Name:    info.Name(),
		ModTime: info.ModTime(),
		Mode:    info.Mode(),
		IsDir:   info.IsDir(),
		Size:    info.Size(),
	}

	dl := r.URL.Query().Get("dl")

	if dl == "" || dl == "0" {
		tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html")))
		w.Header().Set("Content-Type", "text/html; charset=utf-8")

		err := tpl.Execute(w, map[string]interface{}{
			"BaseURL": c.RootURL(),
			"File":    c.File,
		})

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

	return downloadHandler(c, w, r)
}

// renderJSON prints the JSON version of data to the browser.
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
	marsh, err := json.Marshal(data)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	if _, err := w.Write(marsh); err != nil {
		return http.StatusInternalServerError, err
	}

	return 0, nil
}

// matchURL checks if the first URL matches the second.
func matchURL(first, second string) bool {
	first = strings.ToLower(first)
	second = strings.ToLower(second)

	return strings.HasPrefix(first, second)
}

// ErrorToHTTP converts errors to HTTP Status Code.
func ErrorToHTTP(err error, gone bool) int {
	switch {
	case err == nil:
		return http.StatusOK
	case os.IsPermission(err):
		return http.StatusForbidden
	case os.IsNotExist(err):
		if !gone {
			return http.StatusNotFound
		}

		return http.StatusGone
	case os.IsExist(err):
		return http.StatusConflict
	default:
		return http.StatusInternalServerError
	}
}
