package main

import (
	"fmt"
	"log"
	"os"
	"io"
	"path"
	"path/filepath"
	"sort"
	"net/http"
	"html/template"
	"flag"
	"database/sql"
	"encoding/json"
	"encoding/base64"
	"time"
	"archive/zip"
	"crypto/rand"
	"crypto/cipher"
	"crypto/aes"
)

var store = b64decodeAndInitNonce(os.Getenv("SESSION_KEY"))

var nonce []byte = nil

var db *sql.DB

const baseDocDir string = "docs"

type Doc struct {
	Name string
	Size string
	ModTime time.Time
	Link string
}

var fmap template.FuncMap = template.FuncMap {
	"add": func(i, j int) int {
		return i + j
	},
	"formatmodtime": func(i time.Time) string {
		return i.Format("2006-01-02")
	},
}

func b64decodeAndInitNonce(b string) []byte {
	dst := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
	n, err := base64.StdEncoding.Decode(dst, []byte(b))
	if err != nil {
		panic(err)
	}
	dst = dst[:n]

	if nonce == nil {
		nonce = make([]byte, 12)
		if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
			panic(err)
		}
	}

	return dst
}

func encrypt(b []byte) []byte {
	blk, err := aes.NewCipher(store)
	if err != nil {
		panic(err)
	}

	aesgcm, err := cipher.NewGCM(blk)
	if err != nil {
		panic(err)
	}
	return aesgcm.Seal(nil, nonce, b, nil)
}

func decrypt(b []byte) ([]byte, error) {
	blk, err := aes.NewCipher(store)
	if err != nil {
		return nil, err
	}

	aesgcm, err := cipher.NewGCM(blk)
	if err != nil {
		return nil, err
	}

	plain, err := aesgcm.Open(nil, nonce, b, nil)
	if err != nil {
		return nil, err
	}
	return plain, nil
}

func serveTemplate(w http.ResponseWriter, r *http.Request, data interface{}, view ...string) {
	var nav string = "templates/nav.html"
	d := struct {
		Data interface{}
		User *User
	} {
		data,
		nil,
	}
	if u, err := checkSession(w, r); u != nil && err == nil {
		d.User = u
		nav = "templates/nav_logged.html"
	}
	views := []string {"templates/base.html", nav}
	views = append(views, view...)
	t, err := template.New("base.html").Funcs(fmap).ParseFiles(views...)
	if err != nil {
		log.Fatal(err)
	}
	if err := t.Execute(w, d); err != nil {
		log.Fatal(err)
	}
}

func serveSimple(w http.ResponseWriter, r *http.Request, data interface{}, view string, xviews ...string) {
	views := []string {view}
	views = append(views, xviews...)
	fp := filepath.Base(views[len(views)-1])
	t, err := template.New(fp).Funcs(fmap).ParseFiles(views...)
	if err != nil {
		log.Fatal(err)
	}
	if err := t.Execute(w, data); err != nil {
		log.Fatal(err)
	}
}

func checkSession(w http.ResponseWriter, r *http.Request) (*User, error) {
	cookie, err := r.Cookie("session")
	if err != nil {
		if err == http.ErrNoCookie {
			return nil, nil
		} else {
			return nil, err
		}
	}
	ub64, err := base64.StdEncoding.DecodeString(cookie.Value)
	if err != nil {
		return nil, err
	}
	plain, err := decrypt(ub64)
	if err != nil {
		return nil, err
	}
	var user User
	err = json.Unmarshal(plain, &user)
	if err != nil {
		return nil, err
	}
	return &user, nil
}

func sendError(w http.ResponseWriter, r *http.Request, s string, status int) {
	log.Println(s)
	w.WriteHeader(status)
	w.Write([]byte(s))
	//http.Error(w, s, status)
	//view := fmt.Sprintf("templates/%d.html", status)
	//t, err := template.ParseFiles("templates/base.html", view)
	//if err != nil {
	//	log.Fatal(err)
	//}
	//if err := t.Execute(w, nil); err != nil {
	//	log.Fatal(err)
	//}
}

func serveLogin(w http.ResponseWriter, r *http.Request, errorMsg string) {
	err := errorMsg
	if errorMsg == "" {
		err = consumeFlash(w, r, "error")
	}
	data := struct {
		Error string
	}{
		err,
	}
	serveTemplate(w, r, data, "templates/login.html")
}

func unauthorized(w http.ResponseWriter, r *http.Request) {
	sendFlash(w, r, "redirect", r.URL.String())
	w.WriteHeader(http.StatusUnauthorized)
	serveLogin(w, r, "")
}

func sendInvalidMethod(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusMethodNotAllowed)
}

func humanize(i int64) string {
	var sizes [4]string = [4]string {"O", "K", "M", "G"}
	j := i
	s := 0
	for j > 1024 && s < len(sizes) {
		j = j / 1024.0
		s++
	}
	return fmt.Sprintf("%v%s", j, sizes[s])
}

func index(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		w.WriteHeader(http.StatusNotFound)
		serveLogin(w, r, "Page not found")
		return
	}
	u, err := checkSession(w, r)
	if u != nil && err == nil {
		username := u.Username
		if u.IsAdmin {
			name := r.URL.Query().Get("user")
			if name != "" {
				username = name
			}
		}
		userDocPath := filepath.Join(baseDocDir, username)
		err := os.Mkdir(userDocPath, 0750)
		if err != nil && !os.IsExist(err) {
			sendError(w, r, err.Error(), http.StatusInternalServerError)
		}
		files, err := os.ReadDir(filepath.Join(baseDocDir, username))
		if err != nil {
			sendError(w, r, err.Error(), http.StatusInternalServerError)
		}
		var docs []Doc
		sort.Slice(files, func(i, j int) bool {
			info1, err := files[i].Info()
			if err != nil {
				return false
			}
			info2, err := files[j].Info()
			if err != nil {
				return false
			}
			return info1.ModTime().After(info2.ModTime())
		})
		for _, file := range files {
			info, err := file.Info()
			if err != nil {
				sendError(w, r, err.Error(), http.StatusInternalServerError)
				return
			}
			docs = append(docs, Doc {
				file.Name(),
				humanize(info.Size()),
				info.ModTime(),
				path.Join(baseDocDir, username, file.Name()),
			})
		}
		flasherr := consumeFlash(w, r, "error")
		userImpersonation := r.URL.Query().Get("user")
		data := struct {
			Docs []Doc
			Error string
			UserImpersonation string
		}{
			docs,
			flasherr,
			userImpersonation,
		}
		serveTemplate(w, r, data, "templates/user.html")
		return
	} else if err != nil {
		log.Println(err)
	}
	unauthorized(w, r)
}

func admin(w http.ResponseWriter, r *http.Request) {
	u, err := checkSession(w, r)
	if u != nil && err == nil && u.IsAdmin {
		serveTemplate(w, r, nil, "templates/admin.html")
	} else if err != nil {
		//sendError(w, r, err.Error(), http.StatusInternalServerError)
		log.Println(err)
		unauthorized(w, r)
	} else {
		unauthorized(w, r)
	}
}

func adminUsers(w http.ResponseWriter, r *http.Request) {
	u, err := checkSession(w, r)
	if u != nil && err == nil && u.IsAdmin {
		users, err := GetUsers(db)
		if err != nil {
			sendError(w, r, err.Error(), http.StatusInternalServerError)
			return
		}
		serveTemplate(w, r, struct {
			Users []User
			UserImpersonation string
		}{
			users,
			"",
		}, "templates/admin/users.html")
	} else if err != nil {
		//sendError(w, r, err.Error(), http.StatusInternalServerError)
		log.Println(err)
		unauthorized(w, r)
	} else {
		//sendError(w, r, "Unauthorized", http.StatusUnauthorized)
		unauthorized(w, r)
	}
}

func createuser(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		err := consumeFlash(w, r, "error")
		data := struct {
			Error string
		}{
			err,
		}
		serveTemplate(w, r, data, "templates/createuser.html")
	case http.MethodPost:
		u := r.FormValue("user")
		email := r.FormValue("email")
		pass := r.FormValue("pass")
		cpass := r.FormValue("cpass")
		isadmin := r.FormValue("isadmin")
		if len(pass) < 10 {
			sendFlash(w, r, "error", "Le mot de passe doit avoir une longeur supérieure ou égale à 10 caractères.")
			http.Redirect(w, r, "/createuser", http.StatusSeeOther)
			return
		}
		if pass != cpass {
			sendFlash(w, r, "error", "Le mot de passe et la confirmation du mot de passe ne sont pas les mêmes.")
			http.Redirect(w, r, "/createuser", http.StatusSeeOther)
			return
		}
		user := User{-1, u, email, pass, isadmin == "on"}
		user, err := CreateUser(db, user)
		if err != nil {
			sendError(w, r, err.Error(), http.StatusInternalServerError)
			return
		}
		http.Redirect(w, r, "/", http.StatusSeeOther)
	default:
		sendInvalidMethod(w, r)
	}
}

func consumeFlash(w http.ResponseWriter, r *http.Request, name string) string {
	cookie, err := r.Cookie(name)
	if err != nil {
		if err == http.ErrNoCookie {
			return ""
		} else {
			return err.Error()
		}
	}
	http.SetCookie(w, &http.Cookie{
		Name: name,
		Value: "",
		MaxAge: -1,
	})
	s, err := base64.StdEncoding.DecodeString(cookie.Value)
	if err != nil {
		return ""
	}
	return string(s)
}

func sendFlash(w http.ResponseWriter, r *http.Request, name, s string) {
	str := base64.StdEncoding.EncodeToString([]byte(s))
	cookie := http.Cookie {
		Name: name,
		Value: str,
		MaxAge: 0,
		// Only https on qutebrowser
		//Secure: true,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
	}
	http.SetCookie(w, &cookie)
}

func logout(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		u, err := checkSession(w, r)
		if u != nil && err == nil {
			http.SetCookie(w, &http.Cookie{
				Name: "session",
				Value: "",
				MaxAge: -1,
			})
		}
		http.Redirect(w, r, "/login", http.StatusSeeOther)
	default:
		sendInvalidMethod(w, r)
	}
}

func login(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		serveLogin(w, r, "")
	case http.MethodPost:
		u := r.FormValue("user")
		pass := r.FormValue("pass")
		user, err := CheckUserPass(db, User{-1, u, "", pass, false})
		if err != nil {
			w.WriteHeader(http.StatusForbidden)
			serveLogin(w, r, "Incorrect login credentials")
			return
		}
		user.Pass = ""
		jsonData, err := json.Marshal(user)
		if err != nil {
			sendError(w, r, err.Error(), http.StatusInternalServerError)
			return
		}
		ciphertext := encrypt(jsonData)
		bStr := base64.StdEncoding.EncodeToString(ciphertext)
		cookie := http.Cookie {
			Name: "session",
			Value: bStr,
			MaxAge: 0,
			// Only https on qutebrowser
			//Secure: true,
			HttpOnly: true,
			SameSite: http.SameSiteStrictMode,
		}
		http.SetCookie(w, &cookie)
		redirectflash := consumeFlash(w, r, "redirect")
		http.Redirect(w, r, redirectflash, http.StatusSeeOther)
	default:
		sendInvalidMethod(w, r)
	}
}

func handleFileServer(dir, prefix string) http.HandlerFunc {
	fs := http.FileServer(http.Dir(baseDocDir))
	hdlr := http.StripPrefix(prefix, fs).ServeHTTP
	return func(w http.ResponseWriter, r *http.Request) {
		u, err := checkSession(w, r)
		if u != nil && err == nil {
			dir := filepath.Dir(r.URL.Path)
			username := filepath.Base(dir)
			if u.Username == username || u.IsAdmin {
				hdlr(w, r)
				return
			}
		}
		unauthorized(w, r)
	}
}

func download(w http.ResponseWriter, r *http.Request) {
	u, err := checkSession(w, r)
	if u != nil && err == nil {
		username := u.Username
		if u.IsAdmin {
			name := r.URL.Query().Get("user")
			if name != "" {
				username = name
			}
		}
		switch r.Method {
		case http.MethodPost:
			r.ParseForm()
			selection := r.Form["selection"]
			if len(selection) == 0 {
				sendFlash(w, r, "error", "Aucun fichier sélectionné")
				http.Redirect(w, r, "/", http.StatusSeeOther)
				return
			}
			contentDisposition := fmt.Sprintf("attachment; filename=\"Documents.zip\"")
			w.Header().Set("Content-Disposition", contentDisposition)
			wr := zip.NewWriter(w)
			defer wr.Close()
			for _, sel := range selection {
				if filepath.Base(filepath.Dir(sel)) == username {
					wrc, err := wr.Create(filepath.Base(sel))
					if err != nil {
						sendError(w, r, err.Error(), http.StatusInternalServerError)
						return
					}
					f, err := os.Open(sel)
					if err != nil {
						sendError(w, r, err.Error(), http.StatusInternalServerError)
						return
					}
					io.Copy(wrc, f)
				}
			}
		case http.MethodGet:
			contentDisposition := fmt.Sprintf("attachment; filename=\"Documents.zip\"")
			w.Header().Set("Content-Disposition", contentDisposition)
			wr := zip.NewWriter(w)
			defer wr.Close()
			files, err := os.ReadDir(filepath.Join(baseDocDir, username))
			if err != nil {
				sendError(w, r, err.Error(), http.StatusInternalServerError)
				return
			}
			for _, file := range files {
				filePath := path.Join(baseDocDir, username, file.Name())
				wrc, err := wr.Create(filepath.Base(filePath))
				if err != nil {
					sendError(w, r, err.Error(), http.StatusInternalServerError)
					return
				}
				f, err := os.Open(filePath)
				if err != nil {
					sendError(w, r, err.Error(), http.StatusInternalServerError)
					return
				}
				io.Copy(wrc, f)
			}
		default:
			sendInvalidMethod(w, r)
		}
	} else {
		unauthorized(w, r)
	}
}

func upload(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodPost:
		u, err := checkSession(w, r)
		if u != nil && err == nil {
			userImpersonation := false
			username := u.Username
			if u.IsAdmin {
				name := r.URL.Query().Get("user")
				if name != "" {
					username = name
					userImpersonation = true
				}
			}
			userDocPath := filepath.Join(baseDocDir, username)
			err := os.Mkdir(userDocPath, 0750)
			if err != nil && !os.IsExist(err) {
				sendError(w, r, err.Error(), http.StatusInternalServerError)
			}
			rd, err := r.MultipartReader()
			if err != nil {
				sendError(w, r, err.Error(), http.StatusInternalServerError)
			}
			for {
				part, err := rd.NextPart()
				if err == io.EOF {
					break
				} else if err != nil {
					sendError(w, r, err.Error(), http.StatusInternalServerError)
				}
				docPath := filepath.Join(userDocPath, part.FileName())
				file, err := os.Create(docPath)
				if err != nil {
					sendError(w, r, err.Error(), http.StatusInternalServerError)
				}
				defer file.Close()
				_, err = io.Copy(file, part)
				if err != nil {
					sendError(w, r, err.Error(), http.StatusInternalServerError)
				}
			}
			if userImpersonation {
				http.Redirect(w, r, fmt.Sprintf("/?user=%s", username), http.StatusSeeOther)
			} else {
				http.Redirect(w, r, "/", http.StatusSeeOther)
			}
		} else {
			unauthorized(w, r)
		}
	default:
		sendInvalidMethod(w, r)
	}
}

func main() {
	p := flag.Int("p", 8080, "the port to bind to")
	flag.Parse()
	var err error
	db, err = InitAndGetDB("sqlite3", "./db/test.db")
	if err != nil {
		panic(err)
	}
	defer db.Close() 

	http.HandleFunc("/docs/", handleFileServer(baseDocDir, "/docs/"))
	//http.Handle("/docs/", http.StripPrefix("/docs/", http.FileServer(http.Dir("docs"))))
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
	http.HandleFunc("/", index)
	http.HandleFunc("/createuser", createuser)
	http.HandleFunc("/login", login)
	http.HandleFunc("/logout", logout)
	http.HandleFunc("/upload", upload)
	http.HandleFunc("/download", download)
	http.HandleFunc("/imgs", imgs)
	http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, "./robots.txt")
	})
	http.HandleFunc("/admin", admin)
	http.HandleFunc("/admin/users", adminUsers)
	log.Printf("Serving http://localhost:%d\n", *p)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *p), nil))
}
