// Copyright (C) 2019 Marius Schellenberger

package server

import (
	"errors"
	"git.giftfish.de/ston1th/gowiki/pkg/core"
	"git.giftfish.de/ston1th/gowiki/pkg/log"
	"git.giftfish.de/ston1th/jwt/v3"
	"github.com/gorilla/mux"
	"html/template"
	"net/http"
	"net/url"
	"strconv"
	"time"
)

const (
	userClaim        = "user"
	adminClaim       = "admin"
	userCreatedClaim = "user_created"
	pageCreatedClaim = "page_created"
	sharedClaim      = "shared"
	sectionClaim     = "section"
	titleClaim       = "title"
	totpClaim        = "totp"
	rememberClaim    = "remember"
	refererClaim     = "referer"

	empty = "(empty)"
)

func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) *Context {
	h := w.Header()
	h.Set("X-Frame-Options", "DENY")
	h.Set("X-Content-Type-Options", "nosniff")
	h.Set("X-XSS-Protection", "1; mode=block")
	h.Set("Content-Security-Policy", "default-src 'none';style-src 'self';img-src 'self' https: data:;connect-src 'self';frame-ancestors 'none'")
	h.Set("Referrer-Policy", "same-origin")
	return &Context{
		Request:  r,
		Response: w,
		Srv:      s,
		Time:     time.Now(),
	}
}

type Context struct {
	Request  *http.Request
	Response http.ResponseWriter
	Srv      *HTTPServer
	T        *template.Template
	Status   int
	Err      error
	Time     time.Time

	Token jwt.Token

	Data webData
}

type webData struct {
	Version string
	Time    string

	Title     string
	BodyTitle string

	Admin bool
	Login bool

	Key string

	User   string
	Token  string
	Msg    string
	Search string

	Data interface{}
}

type loginData struct {
	User    string
	Referer string
}

type sectionData struct {
	Name    string
	Members map[string]bool
}

type pageData struct {
	Page     core.Page
	Sections map[string]bool
}

func (c *Context) Exec() {
	defer c.log()
	if c.T == nil {
		c.Status = http.StatusInternalServerError
		c.Err = errors.New("template is nil")
		return
	}
	sig := c.Token.RawSig()
	if len(sig) >= keySize {
		c.Data.Token = newXsrf(sig[:keySize])
	}

	c.Data.Version = c.Srv.Config.Version
	if c.Data.BodyTitle == "" {
		c.Data.BodyTitle = c.Data.Title
	}
	c.Data.Admin = c.Admin()
	c.Data.User = c.User()
	c.Data.Login = c.LoggedOn()

	t := strconv.Itoa(int(time.Since(c.Time).Nanoseconds()/1e6)) + "ms"
	if t == "0ms" {
		t = strconv.Itoa(int(time.Since(c.Time).Nanoseconds()/1e5)) + "µs"
	}
	c.Data.Time = t

	if c.Data.Msg != "" {
		c.Status = http.StatusBadRequest
	}

	c.Err = c.T.Execute(c.Response, c.Data)
	if c.Err != nil {
		c.Status = http.StatusInternalServerError
	}
	if c.Status == 0 {
		c.Status = http.StatusOK
	}
}

func (c *Context) log() {
	if c.Err != nil {
		log.Printf("%s %s %s %d error: %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status, c.Err)
		return
	}
	log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status)
}

func (c *Context) Error(i interface{}) {
	switch v := i.(type) {
	case string:
		c.Data.Msg = v
	case error:
		c.Data.Msg = v.Error()
	}
	c.Exec()
}

func (c *Context) NotFound() {
	c.Template("notFoundHandler")
	c.Data = webData{Title: "404"}
	c.Status = http.StatusNotFound
	c.Response.WriteHeader(http.StatusNotFound)
	c.Exec()
}

func (c *Context) Forbidden() {
	c.Template("forbiddenHandler")
	c.Data = webData{Title: "403"}
	c.Status = http.StatusForbidden
	c.Response.WriteHeader(http.StatusForbidden)
	c.Exec()
}

func (c *Context) Template(name string) {
	c.T = c.Srv.templ[name]
}

func (c *Context) Write(buf []byte) (err error) {
	_, err = c.Response.Write(buf)
	return
}

func (c *Context) SetHeader(name, value string) {
	c.Response.Header().Set(name, value)
}

func (c *Context) Redirect(uri string, code int) {
	if len(uri) > 0 && uri[0] != '/' {
		uri = "/" + uri
	}
	log.Printf("%s %s %s %d %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, code, uri)
	http.Redirect(c.Response, c.Request, uri, code)
}

func (c *Context) Method() string {
	return c.Request.Method
}

func (c *Context) Path() string {
	return c.Request.URL.Path
}

func (c *Context) RefererURI() string {
	u, err := url.Parse(c.Request.Header.Get("Referer"))
	if err != nil {
		return ""
	}
	return u.RequestURI()
}

func (c *Context) FormSlice(name string) []string {
	// does nothing if called twice
	c.Request.ParseForm()
	return c.Request.PostForm[name]
}

func (c *Context) Form(name string) string {
	return c.Request.PostFormValue(name)
}

func (c *Context) Var(name string) (ret string) {
	ret, _ = mux.Vars(c.Request)[name]
	return
}

func (c *Context) Forwarded() (ret string) {
	ret = c.Request.Header.Get("X-Forwarded-For")
	if ret == "" {
		ret = empty
	}
	return
}

// CheckXsrf validates the xsrf token
func (c *Context) CheckXsrf() (ok bool) {
	ok = checkXsrf(c.Form("token"), c.Token.RawSig()[:keySize])
	if !ok {
		c.Error("wrong csrf token")
	}
	return
}

func (c *Context) LoggedOn() (ok bool) {
	_, ok = c.Token.Claims.Get(userClaim)
	return
}

func (c *Context) LogSetCookie(msg string, err error) {
	log.Printf("ctx: %s %s\n", msg, err)
	c.SetCookie(nil, 0)
}

func (c *Context) Login(u core.User, remember string) {
	var d time.Duration
	if remember != "" {
		d = day * 7
	}
	c.SetCookie(jwt.Claims{userClaim: u.Username, userCreatedClaim: u.Created, adminClaim: u.Admin}, d)
}

func (c *Context) SetCookie(claims jwt.Claims, d time.Duration) {
	c.setCookie(jwt.NewToken(claims, nil), d)
}

func (c *Context) setCookie(t *jwt.Token, d time.Duration) {
	if d == 0 {
		d = jwt.DefaultExpiry
	}
	t.Claims[jwt.ExpClaim] = jwt.NewExp(d)
	if err := c.Srv.JWT.Sign(t); err != nil {
		log.Println(err)
		return
	}
	c.Token = *t
	cookie := &http.Cookie{
		Name:     cookieName,
		Value:    c.Token.String(),
		Path:     "/",
		MaxAge:   int(d.Seconds()),
		Secure:   c.Srv.Config.SecureCookie,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
	}
	if cookie.MaxAge > 0 {
		cookie.Expires = time.Now().Add(d)
	} else if cookie.MaxAge < 0 {
		cookie.Expires = time.Unix(1, 0)
	}
	http.SetCookie(c.Response, cookie)
}

func (c *Context) User() string {
	return c.Token.Claims.GetString(userClaim)
}

func (c *Context) Totp() string {
	return c.Token.Claims.GetString(totpClaim)
}

func (c *Context) Remember() string {
	return c.Token.Claims.GetString(rememberClaim)
}

func (c *Context) Referer() string {
	return c.Token.Claims.GetString(refererClaim)
}

func (c *Context) Admin() bool {
	return c.Token.Claims.GetBool(adminClaim)
}

type ctxHandler func(*Context)
