/*
 * Copyright © 2018-2021 Musing Studio LLC.
 *
 * This file is part of WriteFreely.
 *
 * WriteFreely is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, included
 * in the LICENSE file in this source code package.
 */

package writefreely

import (
	"fmt"
	"html/template"
	"net/http"
	"net/url"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

	"github.com/gorilla/sessions"
	"github.com/writeas/impart"
	"github.com/writeas/web-core/log"
	"github.com/writefreely/go-gopher"
	"github.com/writefreely/writefreely/config"
	"github.com/writefreely/writefreely/page"
)

// UserLevel represents the required user level for accessing an endpoint
type UserLevel int

const (
	UserLevelNoneType         UserLevel = iota // user or not -- ignored
	UserLevelOptionalType                      // user or not -- object fetched if user
	UserLevelNoneRequiredType                  // non-user (required)
	UserLevelUserType                          // user (required)
)

func UserLevelNone(cfg *config.Config) UserLevel {
	return UserLevelNoneType
}

func UserLevelOptional(cfg *config.Config) UserLevel {
	return UserLevelOptionalType
}

func UserLevelNoneRequired(cfg *config.Config) UserLevel {
	return UserLevelNoneRequiredType
}

func UserLevelUser(cfg *config.Config) UserLevel {
	return UserLevelUserType
}

// UserLevelReader returns the permission level required for any route where
// users can read published content.
func UserLevelReader(cfg *config.Config) UserLevel {
	if cfg.App.Private {
		return UserLevelUserType
	}
	return UserLevelOptionalType
}

type (
	handlerFunc          func(app *App, w http.ResponseWriter, r *http.Request) error
	gopherFunc           func(app *App, w gopher.ResponseWriter, r *gopher.Request) error
	userHandlerFunc      func(app *App, u *User, w http.ResponseWriter, r *http.Request) error
	userApperHandlerFunc func(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error
	dataHandlerFunc      func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error)
	authFunc             func(app *App, r *http.Request) (*User, error)
	UserLevelFunc        func(cfg *config.Config) UserLevel
)

type Handler struct {
	errors       *ErrorPages
	sessionStore sessions.Store
	app          Apper
}

// ErrorPages hold template HTML error pages for displaying errors to the user.
// In each, there should be a defined template named "base".
type ErrorPages struct {
	NotFound            *template.Template
	Gone                *template.Template
	InternalServerError *template.Template
	UnavailableError    *template.Template
	Blank               *template.Template
}

// NewHandler returns a new Handler instance, using the given StaticPage data,
// and saving alias to the application's CookieStore.
func NewHandler(apper Apper) *Handler {
	h := &Handler{
		errors: &ErrorPages{
			NotFound:            template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>404</title></head><body><p>Not found.</p></body></html>{{end}}")),
			Gone:                template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>410</title></head><body><p>Gone.</p></body></html>{{end}}")),
			InternalServerError: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>500</title></head><body><p>Internal server error.</p></body></html>{{end}}")),
			UnavailableError:    template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>503</title></head><body><p>Service is temporarily unavailable.</p></body></html>{{end}}")),
			Blank:               template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>{{.Title}}</title></head><body><p>{{.Content}}</p></body></html>{{end}}")),
		},
		sessionStore: apper.App().SessionStore(),
		app:          apper,
	}

	return h
}

// NewWFHandler returns a new Handler instance, using WriteFreely template files.
// You MUST call writefreely.InitTemplates() before this.
func NewWFHandler(apper Apper) *Handler {
	h := NewHandler(apper)
	h.SetErrorPages(&ErrorPages{
		NotFound:            pages["404-general.tmpl"],
		Gone:                pages["410.tmpl"],
		InternalServerError: pages["500.tmpl"],
		UnavailableError:    pages["503.tmpl"],
		Blank:               pages["blank.tmpl"],
	})
	return h
}

// SetErrorPages sets the given set of ErrorPages as templates for any errors
// that come up.
func (h *Handler) SetErrorPages(e *ErrorPages) {
	h.errors = e
}

// User handles requests made in the web application by the authenticated user.
// This provides user-friendly HTML pages and actions that work in the browser.
func (h *Handler) User(f userHandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleHTTPError(w, r, func() error {
			var status int
			start := time.Now()

			defer func() {
				if e := recover(); e != nil {
					log.Error("%s: %s", e, debug.Stack())
					h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
					status = http.StatusInternalServerError
				}

				log.Info(h.app.ReqLog(r, status, time.Since(start)))
			}()

			u := getUserSession(h.app.App(), r)
			if u == nil {
				err := ErrNotLoggedIn
				status = err.Status
				return err
			}

			err := f(h.app.App(), u, w, r)
			if err == nil {
				status = http.StatusOK
			} else if impErr, ok := err.(impart.HTTPError); ok {
				status = impErr.Status
				if impErr == ErrUserNotFound {
					log.Info("Logged-in user not found. Logging out.")
					sendRedirect(w, http.StatusFound, "/me/logout?to="+h.app.App().cfg.App.LandingPath())
					// Reset err so handleHTTPError does nothing
					err = nil
				}
			} else {
				status = http.StatusInternalServerError
			}

			return err
		}())
	}
}

// Admin handles requests on /admin routes
func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleHTTPError(w, r, func() error {
			var status int
			start := time.Now()

			defer func() {
				if e := recover(); e != nil {
					log.Error("%s: %s", e, debug.Stack())
					h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
					status = http.StatusInternalServerError
				}

				log.Info(h.app.ReqLog(r, status, time.Since(start)))
			}()

			u := getUserSession(h.app.App(), r)
			if u == nil || !u.IsAdmin() {
				err := impart.HTTPError{http.StatusNotFound, ""}
				status = err.Status
				return err
			}

			err := f(h.app.App(), u, w, r)
			if err == nil {
				status = http.StatusOK
			} else if err, ok := err.(impart.HTTPError); ok {
				status = err.Status
			} else {
				status = http.StatusInternalServerError
			}

			return err
		}())
	}
}

// AdminApper handles requests on /admin routes that require an Apper.
func (h *Handler) AdminApper(f userApperHandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleHTTPError(w, r, func() error {
			var status int
			start := time.Now()

			defer func() {
				if e := recover(); e != nil {
					log.Error("%s: %s", e, debug.Stack())
					h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
					status = http.StatusInternalServerError
				}

				log.Info(h.app.ReqLog(r, status, time.Since(start)))
			}()

			u := getUserSession(h.app.App(), r)
			if u == nil || !u.IsAdmin() {
				err := impart.HTTPError{http.StatusNotFound, ""}
				status = err.Status
				return err
			}

			err := f(h.app, u, w, r)
			if err == nil {
				status = http.StatusOK
			} else if err, ok := err.(impart.HTTPError); ok {
				status = err.Status
			} else {
				status = http.StatusInternalServerError
			}

			return err
		}())
	}
}

func apiAuth(app *App, r *http.Request) (*User, error) {
	// Authorize user from Authorization header
	t := r.Header.Get("Authorization")
	if t == "" {
		return nil, ErrNoAccessToken
	}
	u := &User{ID: app.db.GetUserID(t)}
	if u.ID == -1 {
		return nil, ErrBadAccessToken
	}

	return u, nil
}

// optionalAPIAuth is used for endpoints that accept authenticated requests via
// Authorization header or cookie, unlike apiAuth. It returns a different err
// in the case where no Authorization header is present.
func optionalAPIAuth(app *App, r *http.Request) (*User, error) {
	// Authorize user from Authorization header
	t := r.Header.Get("Authorization")
	if t == "" {
		return nil, ErrNotLoggedIn
	}
	u := &User{ID: app.db.GetUserID(t)}
	if u.ID == -1 {
		return nil, ErrBadAccessToken
	}

	return u, nil
}

func webAuth(app *App, r *http.Request) (*User, error) {
	u := getUserSession(app, r)
	if u == nil {
		return nil, ErrNotLoggedIn
	}
	return u, nil
}

// UserAPI handles requests made in the API by the authenticated user.
// This provides user-friendly HTML pages and actions that work in the browser.
func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc {
	return h.UserAll(false, f, apiAuth)
}

// UserWebAPI handles endpoints that accept a user authorized either via the web (cookies) or an Authorization header.
func (h *Handler) UserWebAPI(f userHandlerFunc) http.HandlerFunc {
	return h.UserAll(false, f, func(app *App, r *http.Request) (*User, error) {
		// Authorize user via cookies
		u := getUserSession(app, r)
		if u != nil {
			return u, nil
		}

		// Fall back to access token, since user isn't logged in via web
		var err error
		u, err = apiAuth(app, r)
		if err != nil {
			return nil, err
		}

		return u, nil
	})
}

func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		handleFunc := func() error {
			var status int
			start := time.Now()

			defer func() {
				if e := recover(); e != nil {
					log.Error("%s: %s", e, debug.Stack())
					impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."})
					status = 500
				}

				log.Info(h.app.ReqLog(r, status, time.Since(start)))
			}()

			u, err := a(h.app.App(), r)
			if err != nil {
				if err, ok := err.(impart.HTTPError); ok {
					status = err.Status
				} else {
					status = 500
				}
				return err
			}

			err = f(h.app.App(), u, w, r)
			if err == nil {
				status = 200
			} else if err, ok := err.(impart.HTTPError); ok {
				status = err.Status
			} else {
				status = 500
			}

			return err
		}

		if web {
			h.handleHTTPError(w, r, handleFunc())
		} else {
			h.handleError(w, r, handleFunc())
		}
	}
}

func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc {
	return func(app *App, w http.ResponseWriter, r *http.Request) error {
		err := f(app, w, r)
		if err != nil {
			if ie, ok := err.(impart.HTTPError); ok {
				// Override default redirect with returned error's, if it's a
				// redirect error.
				if ie.Status == http.StatusFound {
					return ie
				}
			}
			return impart.HTTPError{http.StatusFound, loc}
		}
		return nil
	}
}

func (h *Handler) Page(n string) http.HandlerFunc {
	return h.Web(func(app *App, w http.ResponseWriter, r *http.Request) error {
		t, ok := pages[n]
		if !ok {
			return impart.HTTPError{http.StatusNotFound, "Page not found."}
		}

		sp := pageForReq(app, r)

		err := t.ExecuteTemplate(w, "base", sp)
		if err != nil {
			log.Error("Unable to render page: %v", err)
		}
		return err
	}, UserLevelOptional)
}

func (h *Handler) WebErrors(f handlerFunc, ul UserLevelFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// TODO: factor out this logic shared with Web()
		h.handleHTTPError(w, r, func() error {
			var status int
			start := time.Now()

			defer func() {
				if e := recover(); e != nil {
					u := getUserSession(h.app.App(), r)
					username := "None"
					if u != nil {
						username = u.Username
					}
					log.Error("User: %s\n\n%s: %s", username, e, debug.Stack())
					h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
					status = 500
				}

				log.Info(h.app.ReqLog(r, status, time.Since(start)))
			}()

			var session *sessions.Session
			var err error
			if ul(h.app.App().cfg) != UserLevelNoneType {
				session, err = h.sessionStore.Get(r, cookieName)
				if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) {
					// Cookie is required, but we can ignore this error
					log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul(h.app.App().cfg), err)
				}

				_, gotUser := session.Values[cookieUserVal].(*User)
				if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser {
					to := correctPageFromLoginAttempt(r)
					log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
					err := impart.HTTPError{http.StatusFound, to}
					status = err.Status
					return err
				} else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser {
					log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
					err := ErrNotLoggedIn
					status = err.Status
					return err
				}
			}

			// TODO: pass User object to function
			err = f(h.app.App(), w, r)
			if err == nil {
				status = 200
			} else if httpErr, ok := err.(impart.HTTPError); ok {
				status = httpErr.Status
				if status < 300 || status > 399 {
					addSessionFlash(h.app.App(), w, r, httpErr.Message, session)
					return impart.HTTPError{http.StatusFound, r.Referer()}
				}
			} else {
				e := fmt.Sprintf("[Web handler] 500: %v", err)
				if !strings.HasSuffix(e, "write: broken pipe") {
					log.Error(e)
				} else {
					log.Error(e)
				}
				log.Info("Web handler internal error render")
				h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
				status = 500
			}

			return err
		}())
	}
}

func (h *Handler) CollectionPostOrStatic(w http.ResponseWriter, r *http.Request) {
	if strings.Contains(r.URL.Path, ".") && !isRaw(r) {
		start := time.Now()
		status := 200
		defer func() {
			log.Info(h.app.ReqLog(r, status, time.Since(start)))
		}()

		// Serve static file
		h.app.App().shttp.ServeHTTP(w, r)
		return
	}

	h.Web(viewCollectionPost, UserLevelReader)(w, r)
}

// Web handles requests made in the web application. This provides user-
// friendly HTML pages and actions that work in the browser.
func (h *Handler) Web(f handlerFunc, ul UserLevelFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleHTTPError(w, r, func() error {
			var status int
			start := time.Now()

			defer func() {
				if e := recover(); e != nil {
					u := getUserSession(h.app.App(), r)
					username := "None"
					if u != nil {
						username = u.Username
					}
					log.Error("User: %s\n\n%s: %s", username, e, debug.Stack())
					log.Info("Web deferred internal error render")
					h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
					status = 500
				}

				log.Info(h.app.ReqLog(r, status, time.Since(start)))
			}()

			if ul(h.app.App().cfg) != UserLevelNoneType {
				session, err := h.sessionStore.Get(r, cookieName)
				if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) {
					// Cookie is required, but we can ignore this error
					log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul(h.app.App().cfg), err)
				}

				_, gotUser := session.Values[cookieUserVal].(*User)
				if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser {
					to := correctPageFromLoginAttempt(r)
					log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
					err := impart.HTTPError{http.StatusFound, to}
					status = err.Status
					return err
				} else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser {
					log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
					err := ErrNotLoggedIn
					status = err.Status
					return err
				}
			}

			// TODO: pass User object to function
			err := f(h.app.App(), w, r)
			if err == nil {
				status = 200
			} else if httpErr, ok := err.(impart.HTTPError); ok {
				status = httpErr.Status
			} else {
				e := fmt.Sprintf("[Web handler] 500: %v", err)
				log.Error(e)
				log.Info("Web internal error render")
				h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
				status = 500
			}

			return err
		}())
	}
}

func (h *Handler) All(f handlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleError(w, r, func() error {
			// TODO: return correct "success" status
			status := 200
			start := time.Now()

			defer func() {
				if e := recover(); e != nil {
					log.Error("%s:\n%s", e, debug.Stack())
					impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."})
					status = 500
				}

				log.Info(h.app.ReqLog(r, status, time.Since(start)))
			}()

			// TODO: do any needed authentication

			err := f(h.app.App(), w, r)
			if err != nil {
				if err, ok := err.(impart.HTTPError); ok {
					status = err.Status
				} else {
					status = 500
				}
			}

			return err
		}())
	}
}

func (h *Handler) PlainTextAPI(f handlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleTextError(w, r, func() error {
			// TODO: return correct "success" status
			status := 200
			start := time.Now()

			defer func() {
				if e := recover(); e != nil {
					log.Error("%s:\n%s", e, debug.Stack())
					status = http.StatusInternalServerError
					w.WriteHeader(status)
					fmt.Fprintf(w, "Something didn't work quite right. The robots have alerted the humans.")
				}

				log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host))
			}()

			err := f(h.app.App(), w, r)
			if err != nil {
				if err, ok := err.(impart.HTTPError); ok {
					status = err.Status
				} else {
					status = http.StatusInternalServerError
				}
			}

			return err
		}())
	}
}

func (h *Handler) OAuth(f handlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleOAuthError(w, r, func() error {
			// TODO: return correct "success" status
			status := 200
			start := time.Now()

			defer func() {
				if e := recover(); e != nil {
					log.Error("%s:\n%s", e, debug.Stack())
					impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."})
					status = 500
				}

				log.Info(h.app.ReqLog(r, status, time.Since(start)))
			}()

			err := f(h.app.App(), w, r)
			if err != nil {
				if err, ok := err.(impart.HTTPError); ok {
					status = err.Status
				} else {
					status = 500
				}
			}

			return err
		}())
	}
}

func (h *Handler) AllReader(f handlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleError(w, r, func() error {
			status := 200
			start := time.Now()

			defer func() {
				if e := recover(); e != nil {
					log.Error("%s:\n%s", e, debug.Stack())
					impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."})
					status = 500
				}

				log.Info(h.app.ReqLog(r, status, time.Since(start)))
			}()

			// Allow any origin, as public endpoints are handled in here
			w.Header().Set("Access-Control-Allow-Origin", "*")

			if h.app.App().cfg.App.Private {
				// This instance is private, so ensure it's being accessed by a valid user
				// Check if authenticated with an access token
				_, apiErr := optionalAPIAuth(h.app.App(), r)
				if apiErr != nil {
					if err, ok := apiErr.(impart.HTTPError); ok {
						status = err.Status
					} else {
						status = 500
					}

					if apiErr == ErrNotLoggedIn {
						// Fall back to web auth since there was no access token given
						_, err := webAuth(h.app.App(), r)
						if err != nil {
							if err, ok := apiErr.(impart.HTTPError); ok {
								status = err.Status
							} else {
								status = 500
							}
							return err
						}
					} else {
						return apiErr
					}
				}
			}

			err := f(h.app.App(), w, r)
			if err != nil {
				if err, ok := err.(impart.HTTPError); ok {
					status = err.Status
				} else {
					status = 500
				}
			}

			return err
		}())
	}
}

func (h *Handler) Download(f dataHandlerFunc, ul UserLevelFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleHTTPError(w, r, func() error {
			var status int
			start := time.Now()
			defer func() {
				if e := recover(); e != nil {
					log.Error("%s: %s", e, debug.Stack())
					h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
					status = 500
				}

				log.Info(h.app.ReqLog(r, status, time.Since(start)))
			}()

			data, filename, err := f(h.app.App(), w, r)
			if err != nil {
				if err, ok := err.(impart.HTTPError); ok {
					status = err.Status
				} else {
					status = 500
				}
				return err
			}

			ext := ".json"
			ct := "application/json"
			if strings.HasSuffix(r.URL.Path, ".csv") {
				ext = ".csv"
				ct = "text/csv"
			} else if strings.HasSuffix(r.URL.Path, ".zip") {
				ext = ".zip"
				ct = "application/zip"
			}
			w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s%s", filename, ext))
			w.Header().Set("Content-Type", ct)
			w.Header().Set("Content-Length", strconv.Itoa(len(data)))
			fmt.Fprint(w, string(data))

			status = 200
			return nil
		}())
	}
}

func (h *Handler) Redirect(url string, ul UserLevelFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleHTTPError(w, r, func() error {
			start := time.Now()

			var status int
			if ul(h.app.App().cfg) != UserLevelNoneType {
				session, err := h.sessionStore.Get(r, cookieName)
				if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) {
					// Cookie is required, but we can ignore this error
					log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul(h.app.App().cfg), err)
				}

				_, gotUser := session.Values[cookieUserVal].(*User)
				if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser {
					to := correctPageFromLoginAttempt(r)
					log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
					err := impart.HTTPError{http.StatusFound, to}
					status = err.Status
					return err
				} else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser {
					log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
					err := ErrNotLoggedIn
					status = err.Status
					return err
				}
			}

			status = sendRedirect(w, http.StatusFound, url)

			log.Info(h.app.ReqLog(r, status, time.Since(start)))

			return nil
		}())
	}
}

func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err error) {
	if err == nil {
		return
	}

	if err, ok := err.(impart.HTTPError); ok {
		if err.Status >= 300 && err.Status < 400 {
			sendRedirect(w, err.Status, err.Message)
			return
		} else if err.Status == http.StatusUnauthorized {
			q := ""
			if r.URL.RawQuery != "" {
				q = url.QueryEscape("?" + r.URL.RawQuery)
			}
			sendRedirect(w, http.StatusFound, "/login?to="+r.URL.Path+q)
			return
		} else if err.Status == http.StatusGone {
			w.WriteHeader(err.Status)
			p := &struct {
				page.StaticPage
				Content *template.HTML
			}{
				StaticPage: pageForReq(h.app.App(), r),
			}
			if err.Message != "" {
				co := template.HTML(err.Message)
				p.Content = &co
			}
			h.errors.Gone.ExecuteTemplate(w, "base", p)
			return
		} else if err.Status == http.StatusNotFound {
			w.WriteHeader(err.Status)
			if IsActivityPubRequest(r) {
				// This is a fediverse request; simply return the header
				return
			}
			h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
			return
		} else if err.Status == http.StatusInternalServerError {
			w.WriteHeader(err.Status)
			log.Info("handleHTTPErorr internal error render")
			h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
			return
		} else if err.Status == http.StatusServiceUnavailable {
			w.WriteHeader(err.Status)
			h.errors.UnavailableError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
			return
		} else if err.Status == http.StatusAccepted {
			impart.WriteSuccess(w, "", err.Status)
			return
		} else {
			p := &struct {
				page.StaticPage
				Title   string
				Content template.HTML
			}{
				pageForReq(h.app.App(), r),
				fmt.Sprintf("Uh oh (%d)", err.Status),
				template.HTML(fmt.Sprintf("<p style=\"text-align: center\" class=\"introduction\">%s</p>", err.Message)),
			}
			h.errors.Blank.ExecuteTemplate(w, "base", p)
			return
		}
		impart.WriteError(w, err)
		return
	}

	impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
}

func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error) {
	if err == nil {
		return
	}

	if err, ok := err.(impart.HTTPError); ok {
		if err.Status >= 300 && err.Status < 400 {
			sendRedirect(w, err.Status, err.Message)
			return
		}

		//		if strings.Contains(r.Header.Get("Accept"), "text/html") {
		impart.WriteError(w, err)
		//		}
		return
	}

	if IsJSON(r) {
		impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
		return
	}
	h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
}

func (h *Handler) handleTextError(w http.ResponseWriter, r *http.Request, err error) {
	if err == nil {
		return
	}

	if err, ok := err.(impart.HTTPError); ok {
		if err.Status >= 300 && err.Status < 400 {
			sendRedirect(w, err.Status, err.Message)
			return
		}

		w.WriteHeader(err.Status)
		fmt.Fprintf(w, http.StatusText(err.Status))
		return
	}

	w.WriteHeader(http.StatusInternalServerError)
	fmt.Fprintf(w, "This is an unhelpful error message for a miscellaneous internal error.")
}

func (h *Handler) handleOAuthError(w http.ResponseWriter, r *http.Request, err error) {
	if err == nil {
		return
	}

	if err, ok := err.(impart.HTTPError); ok {
		if err.Status >= 300 && err.Status < 400 {
			sendRedirect(w, err.Status, err.Message)
			return
		}

		impart.WriteOAuthError(w, err)
		return
	}

	impart.WriteOAuthError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
	return
}

func correctPageFromLoginAttempt(r *http.Request) string {
	to := r.FormValue("to")
	if to == "" {
		to = "/"
	} else if !strings.HasPrefix(to, "/") {
		to = "/" + to
	}
	return to
}

func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.handleHTTPError(w, r, func() error {
			status := 200
			start := time.Now()

			defer func() {
				if e := recover(); e != nil {
					log.Error("Handler.LogHandlerFunc\n\n%s: %s", e, debug.Stack())
					h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
					status = 500
				}

				// TODO: log actual status code returned
				log.Info(h.app.ReqLog(r, status, time.Since(start)))
			}()

			if h.app.App().cfg.App.Private {
				// This instance is private, so ensure it's being accessed by a valid user
				// Check if authenticated with an access token
				_, apiErr := optionalAPIAuth(h.app.App(), r)
				if apiErr != nil {
					if err, ok := apiErr.(impart.HTTPError); ok {
						status = err.Status
					} else {
						status = 500
					}

					if apiErr == ErrNotLoggedIn {
						// Fall back to web auth since there was no access token given
						_, err := webAuth(h.app.App(), r)
						if err != nil {
							if err, ok := apiErr.(impart.HTTPError); ok {
								status = err.Status
							} else {
								status = 500
							}
							return err
						}
					} else {
						return apiErr
					}
				}
			}

			f(w, r)

			return nil
		}())
	}
}

func (h *Handler) Gopher(f gopherFunc) gopher.HandlerFunc {
	return func(w gopher.ResponseWriter, r *gopher.Request) {
		defer func() {
			if e := recover(); e != nil {
				log.Error("%s: %s", e, debug.Stack())
				w.WriteError("An internal error occurred")
			}
			log.Info("gopher: %s", r.Selector)
		}()

		err := f(h.app.App(), w, r)
		if err != nil {
			log.Error("failed: %s", err)
			w.WriteError("the page failed for some reason (see logs)")
		}
	}
}

func sendRedirect(w http.ResponseWriter, code int, location string) int {
	w.Header().Set("Location", location)
	w.WriteHeader(code)
	return code
}

func cacheControl(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
		next.ServeHTTP(w, r)
	})
}
