// Copyright (C) 2019 Marius Schellenberger

package server

import (
	"git.giftfish.de/ston1th/docstore/pkg/core"
	"git.giftfish.de/ston1th/docstore/pkg/fs"
	"git.giftfish.de/ston1th/docstore/pkg/log"
	"git.giftfish.de/ston1th/docstore/pkg/otp"
	"git.giftfish.de/ston1th/jwt/v3"
	"html/template"
	"net/http"
	"path/filepath"
	"strings"
	"time"
)

const noSuchFile = "no such file or directory"

type notFoundHandler struct {
	s *HTTPServer
}

func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	newContext(w, r, nf.s).NotFound()
}

func totpAuthHandler(h ctxHandler) ctxHandler {
	return func(ctx *Context) {
		if ctx.Totp() != "" {
			h(ctx)
			return
		}
		ctx.Redirect(core.IndexURI, http.StatusFound)
	}
}

func jwtRegisterHandler(h ctxHandler) ctxHandler {
	return func(ctx *Context) {
		if c, err := ctx.Request.Cookie(cookieName); err == nil {
			t, err := jwt.DecodeToken(c.Value)
			if err != nil {
				ctx.LogSetCookie("DecodeToken:", err)
				return
			}
			if err = ctx.Srv.JWT.Verify(t); err != nil {
				ctx.LogSetCookie("VerifyToken:", err)
				return
			}
			ctx.Token = *t
			h(ctx)
			return
		}
		ctx.SetCookie(nil, 0)
		h(ctx)
	}
}

func jwtHandler(h ctxHandler) ctxHandler {
	return func(ctx *Context) {
		if c, err := ctx.Request.Cookie(cookieName); err == nil {
			t, err := jwt.DecodeToken(c.Value)
			if err != nil {
				ctx.LogSetCookie("DecodeToken:", err)
				return
			}
			if err = ctx.Srv.JWT.Verify(t); err != nil {
				ctx.LogSetCookie("VerifyToken:", err)
				return
			}
			path := ctx.Path()
			if t.Claims.GetString(totpClaim) != "" {
				if path == core.TotpURI || path == core.LoginURI || path == core.LogoutURI {
					ctx.Token = *t
					h(ctx)
					return
				}
				ctx.Redirect(core.TotpURI, http.StatusFound)
				return
			}
			ctx.Token = *t
			h(ctx)
			return
		}
		ctx.SetCookie(nil, 0)
		h(ctx)
	}
}

func authHandler(h ctxHandler) ctxHandler {
	return func(ctx *Context) {
		if ctx.LoggedOn() {
			h(ctx)
			return
		}
		ctx.Redirect(core.LoginURI, http.StatusFound)
	}
}

func staticHandler(ctx *Context) {
	var err error
	ctx.SetHeader("Content-Type", "text/css; charset=utf-8")
	ctx.SetHeader("Expires", time.Now().UTC().Add(max).Format(http.TimeFormat))
	switch ctx.Path() {
	case core.BootstrapCSS:
		err = ctx.Write(ctx.Srv.res["bootstrap.css"])
	case core.CustomCSS:
		err = ctx.Write(ctx.Srv.res["custom.css"])
	case core.Favicon:
		ctx.SetHeader("Content-Type", "image/x-icon")
		err = ctx.Write(ctx.Srv.res["favicon.ico"])
	}
	if err != nil {
		log.Println("static:", err)
	}
}

type registerNotFoundHandler struct {
	s *HTTPServer
}

func (nf *registerNotFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	newContext(w, r, nf.s).Redirect(core.IndexURI, http.StatusFound)
}

func registerHandler(ctx *Context) {
	ctx.Template("registerHandler")
	ctx.Data = webData{
		Title:     "Register User",
		BodyTitle: "Register User",
	}
	switch ctx.Method() {
	case "GET":
		ctx.Exec()
	case "POST":
		user := ctx.Form("user")
		password := ctx.Form("password")
		repeat := ctx.Form("repeat")
		ctx.Data.Data = user

		if !ctx.CheckXsrf() {
			return
		}

		if user == "" || password == "" {
			ctx.Error("empty user or password")
			return
		}
		if password != repeat {
			ctx.Error("passwords do not match")
			return
		}
		if err := ctx.Srv.DB.CreateUser(user, password); err != nil {
			ctx.Error(err)
			return
		}
		ctx.SwitchHandler()
	}
}

func rawHandler(ctx *Context) {
	defer ctx.Log()
	path := strings.TrimPrefix(ctx.Path(), core.RawPrefix)
	f, fi, err := ctx.Srv.FS.GetFile(path)
	if err != nil {
		ctx.Status(http.StatusNotFound)
		err = ctx.Write([]byte(noSuchFile))
		if err != nil {
			log.Println("file:", err)
		}
		return
	}
	http.ServeContent(ctx.Response, ctx.Request, fi.Name(), fi.ModTime(), f)
}

func indexHandler(ctx *Context) {
	path := strings.TrimPrefix(ctx.Path(), core.IndexPrefix)
	switch ctx.Method() {
	case "GET":
		earlyErr := make(chan error)
		go func() {
			file, txt, err := ctx.Srv.Scanner.Scan(path)
			if err != nil {
				select {
				case earlyErr <- err:
				}
				return
			}
			id, err := ctx.Srv.DB.Index.Add(txt)
			if err != nil {
				log.Printf("index: %s: %s", path, err)
				return
			}
			err = ctx.Srv.DB.NewFile(id, file)
			if err != nil {
				log.Printf("newFile: %s: %s", path, err)
			}
		}()
		p, _ := filepath.Split(path)
		p = filepath.Dir(p)
		select {
		case err := <-earlyErr:
			ctx.Template("iseHandler")
			ctx.Data = webData{
				Data: template.HTML(strings.ReplaceAll(err.Error(), "\n", "<br>")),
				Path: p,
			}
			ctx.Status(http.StatusInternalServerError)
			ctx.Exec()
			return
		case <-time.After(time.Second):
		}
		ctx.Redirect(p, http.StatusFound)
	}
}

func uploadHandler(ctx *Context) {
	path := strings.TrimPrefix(ctx.Path(), core.UploadPrefix)
	ctx.Data = webData{
		Title:     "Upload",
		BodyTitle: "Upload",
	}
	ctx.Data.Path = path
	ctx.Template("uploadHandler")
	switch ctx.Method() {
	case "GET":
		ctx.Exec()
	case "POST":
		if !ctx.CheckXsrf() {
			return
		}
		f, orig, err := ctx.File("file")
		if err != nil {
			ctx.Error(err)
			return
		}
		newfile, err := ctx.Srv.FS.NewFile(f, path, orig, ctx.Form("name"))
		if err != nil {
			ctx.Error(err)
			return
		}
		ctx.Redirect(newfile, http.StatusFound)
	}
}

func newDirHandler(ctx *Context) {
	path := strings.TrimPrefix(ctx.Path(), core.NewPrefix)
	ctx.Data = webData{
		Title:     "Create Directory",
		BodyTitle: "Create Directory",
	}
	ctx.Data.Path = path
	ctx.Template("newDirHandler")
	switch ctx.Method() {
	case "GET":
		ctx.Exec()
	case "POST":
		if !ctx.CheckXsrf() {
			return
		}
		err := ctx.Srv.FS.Mkdir(path, ctx.Form("name"))
		if err != nil {
			ctx.Error(err)
			return
		}
		ctx.Redirect(path, http.StatusFound)
	}
}

func moveHandler(ctx *Context) {
	path := strings.TrimPrefix(ctx.Path(), core.MovePrefix)
	mode, err := ctx.Srv.FS.Mode(path)
	if err != nil {
		ctx.Error(err)
		return
	}
	switch mode {
	case fs.IsFile:
		ctx.Data = webData{
			Title:     "Move File",
			BodyTitle: "Move File",
		}
	case fs.IsDir:
		ctx.Data = webData{
			Title:     "Move Directory",
			BodyTitle: "Move Directory",
		}
	default:
		ctx.Data = webData{
			Title:     "Move",
			BodyTitle: "Move",
		}
		ctx.Error(noSuchFile)
		return
	}
	ctx.Data.Path = path
	ctx.Data.Paths = ctx.Srv.FS.Recursive(path)
	_, ctx.Data.Data = filepath.Split(path)
	ctx.Template("moveHandler")
	switch ctx.Method() {
	case "GET":
		ctx.Exec()
	case "POST":
		if !ctx.CheckXsrf() {
			return
		}
		name := ctx.Form("name")
		dest := ctx.Form("destination")
		newfile, err := ctx.Srv.FS.Move(path, dest, name)
		if err != nil {
			ctx.Error(err)
			return
		}
		err = ctx.Srv.DB.MoveFile(path, newfile)
		if err != nil {
			ctx.Error(err)
			return
		}
		ctx.Redirect(newfile, http.StatusFound)
	}
}

func deleteHandler(ctx *Context) {
	path := strings.TrimPrefix(ctx.Path(), core.DeletePrefix)
	mode, err := ctx.Srv.FS.Mode(path)
	if err != nil {
		ctx.Error(err)
		return
	}
	switch mode {
	case fs.IsFile:
		ctx.Data = webData{
			Title:     "Delete File",
			BodyTitle: "Delete File",
		}
	case fs.IsDir:
		ctx.Data = webData{
			Title:     "Delete Directory",
			BodyTitle: "Delete Directory",
		}
	default:
		ctx.Data = webData{
			Title:     "Delete",
			BodyTitle: "Delete",
		}
		ctx.Error(noSuchFile)
		return
	}
	ctx.Data.Path = path
	ctx.Data.Data = mode
	ctx.Template("deleteHandler")
	switch ctx.Method() {
	case "GET":
		ctx.Exec()
	case "POST":
		if !ctx.CheckXsrf() {
			return
		}
		files, err := ctx.Srv.FS.Delete(path)
		if err != nil {
			ctx.Error(err)
			return
		}
		for _, f := range files {
			id, err := ctx.Srv.DB.DeleteFile(f)
			if err != nil {
				log.Printf("db: delete: %s %s", f, err)
			}
			log.Debugf("delete %s id: %s from index", f, id)
			if id == "" {
				continue
			}
			err = ctx.Srv.DB.Index.Delete(id)
			if err != nil {
				log.Printf("index: delete: %s %s", f, err)
			}
		}
		p, _ := filepath.Split(path)
		p = filepath.Dir(p)
		ctx.Redirect(p, http.StatusFound)
	}
}

func dirHandler(ctx *Context) {
	ctx.Template("dirHandler")
	path := ctx.Path()
	switch ctx.Method() {
	case "GET":
		mode, err := ctx.Srv.FS.Mode(path)
		if err != nil {
			ctx.Error(err)
			return
		}
		switch mode {
		case fs.IsDir:
			ctx.Data = webData{
				Title: "Directory Viewer",
			}
			dir := ctx.Srv.FS.ReadDir(path)
			ctx.Srv.DB.GetIndexed(dir.Files)
			ctx.Data.Path = path
			ctx.Data.Data = dir
			ctx.Data.Paths = fs.Paths(path)
			ctx.Exec()
		case fs.IsFile:
			ctx.Data = webData{
				Title: "Document Viewer",
			}
			ctx.Template("fileHandler")
			ctx.Data.Data = core.Path{
				Name: path,
				Abs:  core.RawPrefix + path,
				Flag: ctx.Srv.DB.IsIndexed(path),
			}
			ctx.Data.Paths = fs.Paths(path)
			ctx.Exec()
		default:
			ctx.Data = webData{
				Title: "Viewer",
			}
			ctx.Error(noSuchFile)
		}
	}
}

func loginHandler(ctx *Context) {
	if ctx.LoggedOn() {
		ctx.Redirect(core.IndexURI, http.StatusFound)
		return
	}
	ctx.Template("loginHandler")
	ctx.Data = webData{
		Title:     "Login",
		BodyTitle: "Login",
	}
	var data loginData
	switch ctx.Method() {
	case "GET":
		data.Referer = ctx.RefererURI()
		ctx.Data.Data = data
		ctx.Exec()
	case "POST":
		if !ctx.CheckXsrf() {
			return
		}
		data.User = ctx.Form("user")
		data.Referer = ctx.Form("referer")
		ctx.Data.Data = data
		if data.Referer == "" {
			data.Referer = core.IndexURI
		}
		password := ctx.Form("password")
		if data.User == "" || password == "" {
			ctx.Error("empty username or password")
			return
		}
		u, err := ctx.Srv.DB.Login(data.User, password)
		if err != nil {
			log.Println("login:", data.User, err)
			ctx.Error("bad username or password")
			return
		}
		if u.Secret == "" {
			log.Println("login:", u.Username)
			ctx.Login(u, ctx.Form("remember"))
			ctx.Redirect(data.Referer, http.StatusFound)
			return
		}
		ctx.SetCookie(jwt.Claims{
			totpClaim:     u.Username,
			rememberClaim: ctx.Form("remember"),
			refererClaim:  data.Referer,
		}, time.Minute)
		ctx.Redirect(core.TotpURI, http.StatusFound)
	}
}

func loginTotpHandler(ctx *Context) {
	if ctx.LoggedOn() {
		ctx.Redirect(core.IndexURI, http.StatusFound)
		return
	}
	ctx.Template("loginTotpHandler")
	ctx.Data = webData{
		Title:     "TOTP Verification",
		BodyTitle: "TOTP Veriftcation",
	}
	switch ctx.Method() {
	case "GET":
		ctx.Exec()
	case "POST":
		if !ctx.CheckXsrf() {
			return
		}
		pin := ctx.Form("pin")
		if pin == "" {
			ctx.Error("empty pin")
			return
		}
		u, err := ctx.Srv.DB.Totp(pin)
		if err != nil {
			ctx.Error(err)
			return
		}
		ref := ctx.Referer()
		log.Println("login:", u.Username)
		ctx.Login(u, ctx.Remember())
		ctx.Redirect(ref, http.StatusFound)
	}
}

func searchHandler(ctx *Context) {
	ctx.Template("searchHandler")
	ctx.Data = webData{
		Title:     "Search",
		BodyTitle: "Search",
	}
	switch ctx.Method() {
	case "GET":
		ctx.Exec()
	case "POST":
		if !ctx.CheckXsrf() {
			return
		}
		ctx.Data.Search = ctx.Form("search")
		if ctx.Data.Search == "" {
			ctx.Error("empty search request")
			return
		}
		res, err := ctx.Srv.DB.Index.Search(ctx.Data.Search)
		if err != nil {
			ctx.Error(err)
			return
		}
		err = ctx.Srv.DB.GetPaths(res)
		if err != nil {
			ctx.Error(err)
			return
		}
		ctx.Data.Data = res
		ctx.Exec()
	}
}

func userEditHandler(ctx *Context) {
	ctx.Template("userEditHandler")
	ctx.Data = webData{
		Title:     "Edit User",
		BodyTitle: "Edit User",
	}
	u, err := ctx.Srv.DB.GetUserWithoutPassword()
	if err != nil {
		ctx.Error(err)
		return
	}
	ctx.Data.Data = u
	switch ctx.Method() {
	case "GET":
		ctx.Exec()
	case "POST":
		if !ctx.CheckXsrf() {
			return
		}
		password := ctx.Form("password")
		repeat := ctx.Form("repeat")

		if password != repeat {
			ctx.Error("passwords do not match")
			return
		}
		if err := ctx.Srv.DB.UpdateUserPassword(password); err != nil {
			ctx.Error(err)
			return
		}
		log.Printf("update: %s updated\n", ctx.User())
		ctx.Redirect(core.IndexURI, http.StatusFound)
	}
}

func userTotpHandler(ctx *Context) {
	ctx.Template("userTotpHandler")
	ctx.Data = webData{
		Title:     "TOTP",
		BodyTitle: "TOTP",
	}
	req := otp.Request{Username: ctx.User()}
	u, err := ctx.Srv.DB.GetUser()
	if err != nil {
		ctx.Error(err)
		return
	}
	if u.Secret == "" {
		switch ctx.Method() {
		case "GET":
			req, err = otp.New(req.Username)
			if err != nil {
				ctx.Error(err)
				return
			}
		case "POST":
			req.URL = ctx.Form("url")
			req.Image, req.Secret, err = otp.ImageSecretFromURL(req.URL)
			if err != nil {
				ctx.Error(err)
				return
			}
		}
	}
	switch ctx.Method() {
	case "GET":
		ctx.Data.Data = req
		ctx.Exec()
	case "POST":
		if !ctx.CheckXsrf() {
			return
		}
		pin := ctx.Form("pin")
		if pin == "" {
			ctx.Error("empty pin")
			return
		}
		ctx.Data.Data = req
		secret := req.Secret
		if u.Secret != "" {
			secret = u.Secret
		}
		user := ctx.User()

		if !otp.Validate(pin, secret) {
			ctx.Error("validation failed")
			return
		}

		if err := ctx.Srv.DB.UpdateUserSecret(req.Secret); err != nil {
			ctx.Error(err)
			return
		}
		if req.Secret == "" {
			log.Printf("totp: disabled for %s\n", user)
		} else {
			log.Printf("totp: enabled for %s\n", user)
		}
		ctx.Redirect("/user/edit", http.StatusFound)
	}
}

func logoutHandler(ctx *Context) {
	user := ctx.User()
	if user == "" {
		user = ctx.Totp() + " (totp)"
	}
	log.Println("logout:", user)
	ctx.Srv.JWT.Invalidate(&ctx.Token)
	ctx.SetCookie(nil, 0)
	ctx.Redirect(core.LoginURI, http.StatusFound)
}
