package filemanager

import (
	"encoding/json"
	"errors"
	"net/http"
	"os"
	"sort"
	"strconv"
	"strings"

	"github.com/asdine/storm"
)

type modifyRequest struct {
	What  string `json:"what"`  // Answer to: what data type?
	Which string `json:"which"` // Answer to: which field?
}

type modifyUserRequest struct {
	*modifyRequest
	Data *User `json:"data"`
}

// usersHandler is the entry point of the users API. It's just a router
// to send the request to its
func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
	// If the user isn't admin and isn't making a PUT
	// request, then return forbidden.
	if !c.User.Admin && r.Method != http.MethodPut {
		return http.StatusForbidden, nil
	}

	switch r.Method {
	case http.MethodGet:
		return usersGetHandler(c, w, r)
	case http.MethodPost:
		return usersPostHandler(c, w, r)
	case http.MethodDelete:
		return usersDeleteHandler(c, w, r)
	case http.MethodPut:
		return usersPutHandler(c, w, r)
	}

	return http.StatusNotImplemented, nil
}

// getUserID returns the id from the user which is present
// in the request url. If the url is invalid and doesn't
// contain a valid ID, it returns an error.
func getUserID(r *http.Request) (int, error) {
	// Obtains the ID in string from the URL and converts
	// it into an integer.
	sid := strings.TrimPrefix(r.URL.Path, "/")
	sid = strings.TrimSuffix(sid, "/")
	id, err := strconv.Atoi(sid)
	if err != nil {
		return http.StatusBadRequest, err
	}

	return id, nil
}

// getUser returns the user which is present in the request
// body. If the body is empty or the JSON is invalid, it
// returns an error.
func getUser(r *http.Request) (*User, string, error) {
	// Checks if the request body is empty.
	if r.Body == nil {
		return nil, "", errEmptyRequest
	}

	// Parses the request body and checks if it's well formed.
	mod := &modifyUserRequest{}
	err := json.NewDecoder(r.Body).Decode(mod)
	if err != nil {
		return nil, "", err
	}

	// Checks if the request type is right.
	if mod.What != "user" {
		return nil, "", errWrongDataType
	}

	return mod.Data, mod.Which, nil
}

func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
	// Request for the default user data.
	if r.URL.Path == "/base" {
		return renderJSON(w, c.DefaultUser)
	}

	// Request for the listing of users.
	if r.URL.Path == "/" {
		users := []User{}

		for _, user := range c.Users {
			// Copies the user info and removes its
			// password so it won't be sent to the
			// front-end.
			u := *user
			u.Password = ""
			users = append(users, u)
		}

		sort.Slice(users, func(i, j int) bool {
			return users[i].ID < users[j].ID
		})

		return renderJSON(w, users)
	}

	id, err := getUserID(r)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	// Searches for the user and prints the one who matches.
	for _, user := range c.Users {
		if user.ID != id {
			continue
		}

		u := *user
		u.Password = ""
		return renderJSON(w, u)
	}

	// If there aren't any matches, return not found.
	return http.StatusNotFound, errUserNotExist
}

func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
	if r.URL.Path != "/" {
		return http.StatusMethodNotAllowed, nil
	}

	u, _, err := getUser(r)
	if err != nil {
		return http.StatusBadRequest, err
	}

	// Checks if username isn't empty.
	if u.Username == "" {
		return http.StatusBadRequest, errEmptyUsername
	}

	// Checks if filesystem isn't empty.
	if u.FileSystem == "" {
		return http.StatusBadRequest, errEmptyScope
	}

	// Checks if password isn't empty.
	if u.Password == "" {
		return http.StatusBadRequest, errEmptyPassword
	}

	// The username, password and scope cannot be empty.
	if u.Username == "" || u.Password == "" || u.FileSystem == "" {
		return http.StatusBadRequest, errors.New("username, password or scope is empty")
	}

	// Initialize rules if they're not initialized.
	if u.Rules == nil {
		u.Rules = []*Rule{}
	}

	// Initialize commands if not initialized.
	if u.Commands == nil {
		u.Commands = []string{}
	}

	// It's a new user so the ID will be auto created.
	if u.ID != 0 {
		u.ID = 0
	}

	// Checks if the scope exists.
	if code, err := checkFS(string(u.FileSystem)); err != nil {
		return code, err
	}

	// Hashes the password.
	pw, err := hashPassword(u.Password)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	u.Password = pw

	// Saves the user to the database.
	err = c.db.Save(u)
	if err == storm.ErrAlreadyExists {
		return http.StatusConflict, errUserExist
	}

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

	// Saves the user to the memory.
	c.Users[u.Username] = u

	// Set the Location header and return.
	w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
	w.WriteHeader(http.StatusCreated)
	return 0, nil
}

func checkFS(path string) (int, error) {
	info, err := os.Stat(path)

	if err != nil {
		if !os.IsNotExist(err) {
			return http.StatusInternalServerError, err
		}

		err = os.MkdirAll(path, 0666)
		if err != nil {
			return http.StatusInternalServerError, err
		}

		return 0, nil
	}

	if !info.IsDir() {
		return http.StatusBadRequest, errors.New("Scope is not a dir")
	}

	return 0, nil
}

func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
	if r.URL.Path == "/" {
		return http.StatusMethodNotAllowed, nil
	}

	id, err := getUserID(r)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	// Deletes the user from the database.
	err = c.db.DeleteStruct(&User{ID: id})
	if err == storm.ErrNotFound {
		return http.StatusNotFound, errUserNotExist
	}

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

	// Delete the user from the in-memory users map.
	for _, user := range c.Users {
		if user.ID == id {
			delete(c.Users, user.Username)
			break
		}
	}

	return http.StatusOK, nil
}

func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
	// New users should be created on /api/users.
	if r.URL.Path == "/" {
		return http.StatusMethodNotAllowed, nil
	}

	// Gets the user ID from the URL and checks if it's valid.
	id, err := getUserID(r)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	// Checks if the user has permission to access this page.
	if !c.User.Admin && id != c.User.ID {
		return http.StatusForbidden, nil
	}

	// Gets the user from the request body.
	u, which, err := getUser(r)
	if err != nil {
		return http.StatusBadRequest, err
	}

	// Updates the CSS and locale.
	if which == "partial" {
		c.User.CSS = u.CSS
		c.User.Locale = u.Locale
		err = c.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
		if err != nil {
			return http.StatusInternalServerError, err
		}

		err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale)
		if err != nil {
			return http.StatusInternalServerError, err
		}

		return http.StatusOK, nil
	}

	// Updates the Password.
	if which == "password" {
		if u.Password == "" {
			return http.StatusBadRequest, errEmptyPassword
		}

		pw, err := hashPassword(u.Password)
		if err != nil {
			return http.StatusInternalServerError, err
		}

		c.User.Password = pw
		err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
		if err != nil {
			return http.StatusInternalServerError, err
		}

		return http.StatusOK, nil
	}

	// If can only be all.
	if which != "all" {
		return http.StatusBadRequest, errInvalidUpdateField
	}

	// Checks if username isn't empty.
	if u.Username == "" {
		return http.StatusBadRequest, errEmptyUsername
	}

	// Checks if filesystem isn't empty.
	if u.FileSystem == "" {
		return http.StatusBadRequest, errEmptyScope
	}

	// Checks if the scope exists.
	if code, err := checkFS(string(u.FileSystem)); err != nil {
		return code, err
	}

	// Initialize rules if they're not initialized.
	if u.Rules == nil {
		u.Rules = []*Rule{}
	}

	// Initialize commands if not initialized.
	if u.Commands == nil {
		u.Commands = []string{}
	}

	// Gets the current saved user from the in-memory map.
	var suser *User
	for _, user := range c.Users {
		if user.ID == id {
			suser = user
			break
		}
	}
	if suser == nil {
		return http.StatusNotFound, nil
	}

	u.ID = id

	// Changes the password if the request wants it.
	if u.Password != "" {
		pw, err := hashPassword(u.Password)
		if err != nil {
			return http.StatusInternalServerError, err
		}

		u.Password = pw
	} else {
		u.Password = suser.Password
	}

	// Updates the whole User struct because we always are supposed
	// to send a new entire object.
	err = c.db.Save(u)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	// If the user changed the username, delete the old user
	// from the in-memory user map.
	if suser.Username != u.Username {
		delete(c.Users, suser.Username)
	}

	c.Users[u.Username] = u
	return http.StatusOK, nil
}