package writefreely

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"html/template"
	"math"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"unicode"

	"github.com/gorilla/mux"
	"github.com/writeas/impart"
	"github.com/writeas/web-core/activitystreams"
	"github.com/writeas/web-core/auth"
	"github.com/writeas/web-core/bots"
	"github.com/writeas/web-core/log"
	waposts "github.com/writeas/web-core/posts"
	"github.com/writeas/writefreely/author"
	"github.com/writeas/writefreely/page"
)

type (
	// TODO: add Direction to db
	// TODO: add Language to db
	Collection struct {
		ID          int64          `datastore:"id" json:"-"`
		Alias       string         `datastore:"alias" schema:"alias" json:"alias"`
		Title       string         `datastore:"title" schema:"title" json:"title"`
		Description string         `datastore:"description" schema:"description" json:"description"`
		Direction   string         `schema:"dir" json:"dir,omitempty"`
		Language    string         `schema:"lang" json:"lang,omitempty"`
		StyleSheet  string         `datastore:"style_sheet" schema:"style_sheet" json:"style_sheet"`
		Script      string         `datastore:"script" schema:"script" json:"script,omitempty"`
		Public      bool           `datastore:"public" json:"public"`
		Visibility  collVisibility `datastore:"private" json:"-"`
		Format      string         `datastore:"format" json:"format,omitempty"`
		Views       int64          `json:"views"`
		OwnerID     int64          `datastore:"owner_id" json:"-"`
		PublicOwner bool           `datastore:"public_owner" json:"-"`
		URL         string         `json:"url,omitempty"`

		db *datastore
	}
	CollectionObj struct {
		Collection
		TotalPosts int           `json:"total_posts"`
		Owner      *User         `json:"owner,omitempty"`
		Posts      *[]PublicPost `json:"posts,omitempty"`
	}
	DisplayCollection struct {
		*CollectionObj
		Prefix      string
		IsTopLevel  bool
		CurrentPage int
		TotalPages  int
		Format      *CollectionFormat
	}
	SubmittedCollection struct {
		// Data used for updating a given collection
		ID      int64
		OwnerID uint64

		// Form helpers
		PreferURL string `schema:"prefer_url" json:"prefer_url"`
		Privacy   int    `schema:"privacy" json:"privacy"`
		Pass      string `schema:"password" json:"password"`
		MathJax   bool   `schema:"mathjax" json:"mathjax"`
		Handle    string `schema:"handle" json:"handle"`

		// Actual collection values updated in the DB
		Alias       *string         `schema:"alias" json:"alias"`
		Title       *string         `schema:"title" json:"title"`
		Description *string         `schema:"description" json:"description"`
		StyleSheet  *sql.NullString `schema:"style_sheet" json:"style_sheet"`
		Script      *sql.NullString `schema:"script" json:"script"`
		Visibility  *int            `schema:"visibility" json:"public"`
		Format      *sql.NullString `schema:"format" json:"format"`
	}
	CollectionFormat struct {
		Format string
	}

	collectionReq struct {
		// Information about the collection request itself
		prefix, alias, domain string
		isCustomDomain        bool

		// User-related fields
		isCollOwner bool
	}
)

func (sc *SubmittedCollection) FediverseHandle() string {
	if sc.Handle == "" {
		return apCustomHandleDefault
	}
	return getSlug(sc.Handle, "")
}

// collVisibility represents the visibility level for the collection.
type collVisibility int

// Visibility levels. Values are bitmasks, stored in the database as
// decimal numbers. If adding types, append them to this list. If removing,
// replace the desired visibility with a new value.
const CollUnlisted collVisibility = 0
const (
	CollPublic collVisibility = 1 << iota
	CollPrivate
	CollProtected
)

func (cf *CollectionFormat) Ascending() bool {
	return cf.Format == "novel"
}
func (cf *CollectionFormat) ShowDates() bool {
	return cf.Format == "blog"
}
func (cf *CollectionFormat) PostsPerPage() int {
	if cf.Format == "novel" {
		return postsPerPage
	}
	return postsPerPage
}

// Valid returns whether or not a format value is valid.
func (cf *CollectionFormat) Valid() bool {
	return cf.Format == "blog" ||
		cf.Format == "novel" ||
		cf.Format == "notebook"
}

// NewFormat creates a new CollectionFormat object from the Collection.
func (c *Collection) NewFormat() *CollectionFormat {
	cf := &CollectionFormat{Format: c.Format}

	// Fill in default format
	if cf.Format == "" {
		cf.Format = "blog"
	}

	return cf
}

func (c *Collection) IsUnlisted() bool {
	return c.Visibility == 0
}

func (c *Collection) IsPrivate() bool {
	return c.Visibility&CollPrivate != 0
}

func (c *Collection) IsProtected() bool {
	return c.Visibility&CollProtected != 0
}

func (c *Collection) IsPublic() bool {
	return c.Visibility&CollPublic != 0
}

func (c *Collection) FriendlyVisibility() string {
	if c.IsPrivate() {
		return "Private"
	}
	if c.IsPublic() {
		return "Public"
	}
	if c.IsProtected() {
		return "Password-protected"
	}
	return "Unlisted"
}

func (c *Collection) ShowFooterBranding() bool {
	// TODO: implement this setting
	return true
}

// CanonicalURL returns a fully-qualified URL to the collection.
func (c *Collection) CanonicalURL() string {
	return c.RedirectingCanonicalURL(false)
}

func (c *Collection) DisplayCanonicalURL() string {
	us := c.CanonicalURL()
	u, err := url.Parse(us)
	if err != nil {
		return us
	}
	p := u.Path
	if p == "/" {
		p = ""
	}
	return u.Hostname() + p
}

func (c *Collection) RedirectingCanonicalURL(isRedir bool) string {
	if isSingleUser {
		return hostName + "/"
	}

	return fmt.Sprintf("%s/%s/", hostName, c.Alias)
}

// PrevPageURL provides a full URL for the previous page of collection posts,
// returning a /page/N result for pages >1
func (c *Collection) PrevPageURL(prefix string, n int, tl bool) string {
	u := ""
	if n == 2 {
		// Previous page is 1; no need for /page/ prefix
		if prefix == "" {
			u = "/"
		}
		// Else leave off trailing slash
	} else {
		u = fmt.Sprintf("/page/%d", n-1)
	}

	if tl {
		return u
	}
	return "/" + prefix + c.Alias + u
}

// NextPageURL provides a full URL for the next page of collection posts
func (c *Collection) NextPageURL(prefix string, n int, tl bool) string {
	if tl {
		return fmt.Sprintf("/page/%d", n+1)
	}
	return fmt.Sprintf("/%s%s/page/%d", prefix, c.Alias, n+1)
}

func (c *Collection) DisplayTitle() string {
	if c.Title != "" {
		return c.Title
	}
	return c.Alias
}

func (c *Collection) StyleSheetDisplay() template.CSS {
	return template.CSS(c.StyleSheet)
}

// ForPublic modifies the Collection for public consumption, such as via
// the API.
func (c *Collection) ForPublic() {
	c.URL = c.CanonicalURL()
}

var isLowerLetter = regexp.MustCompile("[a-z]").MatchString

func (c *Collection) PersonObject(ids ...int64) *activitystreams.Person {
	accountRoot := c.FederatedAccount()
	p := activitystreams.NewPerson(accountRoot)
	p.URL = c.CanonicalURL()
	uname := c.Alias
	p.PreferredUsername = uname
	p.Name = c.DisplayTitle()
	p.Summary = c.Description
	if p.Name != "" {
		if av := c.AvatarURL(); av != "" {
			p.Icon = activitystreams.Image{
				Type:      "Image",
				MediaType: "image/png",
				URL:       av,
			}
		}
	}

	collID := c.ID
	if len(ids) > 0 {
		collID = ids[0]
	}
	pub, priv := c.db.GetAPActorKeys(collID)
	if pub != nil {
		p.AddPubKey(pub)
		p.SetPrivKey(priv)
	}

	return p
}

func (c *Collection) AvatarURL() string {
	fl := string(unicode.ToLower([]rune(c.DisplayTitle())[0]))
	if !isLowerLetter(fl) {
		return ""
	}
	return hostName + "/img/avatars/" + fl + ".png"
}

func (c *Collection) FederatedAPIBase() string {
	return hostName + "/"
}

func (c *Collection) FederatedAccount() string {
	accountUser := c.Alias
	return c.FederatedAPIBase() + "api/collections/" + accountUser
}

func (c *Collection) RenderMathJax() bool {
	return c.db.CollectionHasAttribute(c.ID, "render_mathjax")
}

func newCollection(app *app, w http.ResponseWriter, r *http.Request) error {
	reqJSON := IsJSON(r.Header.Get("Content-Type"))
	alias := r.FormValue("alias")
	title := r.FormValue("title")

	var missingParams, accessToken string
	var u *User
	c := struct {
		Alias string `json:"alias" schema:"alias"`
		Title string `json:"title" schema:"title"`
		Web   bool   `json:"web" schema:"web"`
	}{}
	if reqJSON {
		// Decode JSON request
		decoder := json.NewDecoder(r.Body)
		err := decoder.Decode(&c)
		if err != nil {
			log.Error("Couldn't parse post update JSON request: %v\n", err)
			return ErrBadJSON
		}
	} else {
		// TODO: move form parsing to formDecoder
		c.Alias = alias
		c.Title = title
	}

	if c.Alias == "" {
		if c.Title != "" {
			// If only a title was given, just use it to generate the alias.
			c.Alias = getSlug(c.Title, "")
		} else {
			missingParams += "`alias` "
		}
	}
	if c.Title == "" {
		missingParams += "`title` "
	}
	if missingParams != "" {
		return impart.HTTPError{http.StatusBadRequest, fmt.Sprintf("Parameter(s) %srequired.", missingParams)}
	}

	if reqJSON && !c.Web {
		accessToken = r.Header.Get("Authorization")
		if accessToken == "" {
			return ErrNoAccessToken
		}
	} else {
		u = getUserSession(app, r)
		if u == nil {
			return ErrNotLoggedIn
		}
	}

	if !author.IsValidUsername(app.cfg, c.Alias) {
		return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."}
	}

	var coll *Collection
	var err error
	if accessToken != "" {
		coll, err = app.db.CreateCollectionFromToken(c.Alias, c.Title, accessToken)
		if err != nil {
			// TODO: handle this
			return err
		}
	} else {
		coll, err = app.db.CreateCollection(c.Alias, c.Title, u.ID)
		if err != nil {
			// TODO: handle this
			return err
		}
	}

	res := &CollectionObj{Collection: *coll}

	if reqJSON {
		return impart.WriteSuccess(w, res, http.StatusCreated)
	}
	redirectTo := "/me/c/"
	// TODO: redirect to pad when necessary
	return impart.HTTPError{http.StatusFound, redirectTo}
}

func apiCheckCollectionPermissions(app *app, r *http.Request, c *Collection) (int64, error) {
	accessToken := r.Header.Get("Authorization")
	var userID int64 = -1
	if accessToken != "" {
		userID = app.db.GetUserID(accessToken)
	}
	isCollOwner := userID == c.OwnerID
	if c.IsPrivate() && !isCollOwner {
		// Collection is private, but user isn't authenticated
		return -1, ErrCollectionNotFound
	}
	if c.IsProtected() {
		// TODO: check access token
		return -1, ErrCollectionUnauthorizedRead
	}

	return userID, nil
}

// fetchCollection handles the API endpoint for retrieving collection data.
func fetchCollection(app *app, w http.ResponseWriter, r *http.Request) error {
	accept := r.Header.Get("Accept")
	if strings.Contains(accept, "application/activity+json") {
		return handleFetchCollectionActivities(app, w, r)
	}

	vars := mux.Vars(r)
	alias := vars["alias"]

	// TODO: move this logic into a common getCollection function
	// Get base Collection data
	c, err := app.db.GetCollection(alias)
	if err != nil {
		return err
	}
	// Redirect users who aren't requesting JSON
	reqJSON := IsJSON(r.Header.Get("Content-Type"))
	if !reqJSON {
		return impart.HTTPError{http.StatusFound, c.CanonicalURL()}
	}

	// Check permissions
	userID, err := apiCheckCollectionPermissions(app, r, c)
	if err != nil {
		return err
	}
	isCollOwner := userID == c.OwnerID

	// Fetch extra data about the Collection
	res := &CollectionObj{Collection: *c}
	if c.PublicOwner {
		u, err := app.db.GetUserByID(res.OwnerID)
		if err != nil {
			// Log the error and just continue
			log.Error("Error getting user for collection: %v", err)
		} else {
			res.Owner = u
		}
	}
	app.db.GetPostsCount(res, isCollOwner)
	// Strip non-public information
	res.Collection.ForPublic()

	return impart.WriteSuccess(w, res, http.StatusOK)
}

// fetchCollectionPosts handles an API endpoint for retrieving a collection's
// posts.
func fetchCollectionPosts(app *app, w http.ResponseWriter, r *http.Request) error {
	vars := mux.Vars(r)
	alias := vars["alias"]

	c, err := app.db.GetCollection(alias)
	if err != nil {
		return err
	}

	// Check permissions
	userID, err := apiCheckCollectionPermissions(app, r, c)
	if err != nil {
		return err
	}
	isCollOwner := userID == c.OwnerID

	// Get page
	page := 1
	if p := r.FormValue("page"); p != "" {
		pInt, _ := strconv.Atoi(p)
		if pInt > 0 {
			page = pInt
		}
	}

	posts, err := app.db.GetPosts(c, page, isCollOwner, false)
	if err != nil {
		return err
	}
	coll := &CollectionObj{Collection: *c, Posts: posts}
	app.db.GetPostsCount(coll, isCollOwner)
	// Strip non-public information
	coll.Collection.ForPublic()

	// Transform post bodies if needed
	if r.FormValue("body") == "html" {
		for _, p := range *coll.Posts {
			p.Content = waposts.ApplyMarkdown([]byte(p.Content))
		}
	}

	return impart.WriteSuccess(w, coll, http.StatusOK)
}

type CollectionPage struct {
	page.StaticPage
	*DisplayCollection
	IsCustomDomain bool
	IsWelcome      bool
	IsOwner        bool
	CanPin         bool
	Username       string
	Collections    *[]Collection
	PinnedPosts    *[]PublicPost
}

func (c *CollectionObj) ScriptDisplay() template.JS {
	return template.JS(c.Script)
}

var jsSourceCommentReg = regexp.MustCompile("(?m)^// src:(.+)$")

func (c *CollectionObj) ExternalScripts() []template.URL {
	scripts := []template.URL{}
	if c.Script == "" {
		return scripts
	}

	matches := jsSourceCommentReg.FindAllStringSubmatch(c.Script, -1)
	for _, m := range matches {
		scripts = append(scripts, template.URL(strings.TrimSpace(m[1])))
	}
	return scripts
}

func (c *CollectionObj) CanShowScript() bool {
	return false
}

func processCollectionRequest(cr *collectionReq, vars map[string]string, w http.ResponseWriter, r *http.Request) error {
	cr.prefix = vars["prefix"]
	cr.alias = vars["collection"]
	// Normalize the URL, redirecting user to consistent post URL
	if cr.alias != strings.ToLower(cr.alias) {
		return impart.HTTPError{http.StatusMovedPermanently, fmt.Sprintf("/%s/", strings.ToLower(cr.alias))}
	}

	return nil
}

// processCollectionPermissions checks the permissions for the given
// collectionReq, returning a Collection if access is granted; otherwise this
// renders any necessary collection pages, for example, if requesting a custom
// domain that doesn't yet have a collection associated, or if a collection
// requires a password. In either case, this will return nil, nil -- thus both
// values should ALWAYS be checked to determine whether or not to continue.
func processCollectionPermissions(app *app, cr *collectionReq, u *User, w http.ResponseWriter, r *http.Request) (*Collection, error) {
	// Display collection if this is a collection
	var c *Collection
	var err error
	if app.cfg.App.SingleUser {
		c, err = app.db.GetCollectionByID(1)
	} else {
		c, err = app.db.GetCollection(cr.alias)
	}
	// TODO: verify we don't reveal the existence of a private collection with redirection
	if err != nil {
		if err, ok := err.(impart.HTTPError); ok {
			if err.Status == http.StatusNotFound {
				if cr.isCustomDomain {
					// User is on the site from a custom domain
					//tErr := pages["404-domain.tmpl"].ExecuteTemplate(w, "base", pageForHost(page.StaticPage{}, r))
					//if tErr != nil {
					//log.Error("Unable to render 404-domain page: %v", err)
					//}
					return nil, nil
				}
				if len(cr.alias) >= minIDLen && len(cr.alias) <= maxIDLen {
					// Alias is within post ID range, so just be sure this isn't a post
					if app.db.PostIDExists(cr.alias) {
						// TODO: use StatusFound for vanity post URLs when we implement them
						return nil, impart.HTTPError{http.StatusMovedPermanently, "/" + cr.alias}
					}
				}
				// Redirect if necessary
				newAlias := app.db.GetCollectionRedirect(cr.alias)
				if newAlias != "" {
					return nil, impart.HTTPError{http.StatusFound, "/" + newAlias + "/"}
				}
			}
		}
		return nil, err
	}

	// Update CollectionRequest to reflect owner status
	cr.isCollOwner = u != nil && u.ID == c.OwnerID

	// Check permissions
	if !cr.isCollOwner {
		if c.IsPrivate() {
			return nil, ErrCollectionNotFound
		} else if c.IsProtected() {
			uname := ""
			if u != nil {
				uname = u.Username
			}

			// See if we've authorized this collection
			authd := isAuthorizedForCollection(app, c.Alias, r)

			if !authd {
				p := struct {
					page.StaticPage
					*CollectionObj
					Username string
					Next     string
					Flashes  []template.HTML
				}{
					StaticPage:    pageForReq(app, r),
					CollectionObj: &CollectionObj{Collection: *c},
					Username:      uname,
					Next:          r.FormValue("g"),
					Flashes:       []template.HTML{},
				}
				// Get owner information
				p.CollectionObj.Owner, err = app.db.GetUserByID(c.OwnerID)
				if err != nil {
					// Log the error and just continue
					log.Error("Error getting user for collection: %v", err)
				}

				flashes, _ := getSessionFlashes(app, w, r, nil)
				for _, flash := range flashes {
					p.Flashes = append(p.Flashes, template.HTML(flash))
				}
				err = templates["password-collection"].ExecuteTemplate(w, "password-collection", p)
				if err != nil {
					log.Error("Unable to render password-collection: %v", err)
					return nil, err
				}
				return nil, nil
			}
		}
	}
	return c, nil
}

func checkUserForCollection(app *app, cr *collectionReq, r *http.Request, isPostReq bool) (*User, error) {
	u := getUserSession(app, r)
	return u, nil
}

func newDisplayCollection(c *Collection, cr *collectionReq, page int) *DisplayCollection {
	coll := &DisplayCollection{
		CollectionObj: &CollectionObj{Collection: *c},
		CurrentPage:   page,
		Prefix:        cr.prefix,
		IsTopLevel:    isSingleUser,
		Format:        c.NewFormat(),
	}
	c.db.GetPostsCount(coll.CollectionObj, cr.isCollOwner)
	return coll
}

func getCollectionPage(vars map[string]string) int {
	page := 1
	var p int
	p, _ = strconv.Atoi(vars["page"])
	if p > 0 {
		page = p
	}
	return page
}

// handleViewCollection displays the requested Collection
func handleViewCollection(app *app, w http.ResponseWriter, r *http.Request) error {
	vars := mux.Vars(r)
	cr := &collectionReq{}

	err := processCollectionRequest(cr, vars, w, r)
	if err != nil {
		return err
	}

	u, err := checkUserForCollection(app, cr, r, false)
	if err != nil {
		return err
	}

	page := getCollectionPage(vars)

	c, err := processCollectionPermissions(app, cr, u, w, r)
	if c == nil || err != nil {
		return err
	}

	// Serve ActivityStreams data now, if requested
	if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
		ac := c.PersonObject()
		ac.Context = []interface{}{activitystreams.Namespace}
		return impart.RenderActivityJSON(w, ac, http.StatusOK)
	}

	// Fetch extra data about the Collection
	// TODO: refactor out this logic, shared in collection.go:fetchCollection()
	coll := newDisplayCollection(c, cr, page)

	coll.TotalPages = int(math.Ceil(float64(coll.TotalPosts) / float64(coll.Format.PostsPerPage())))
	if coll.TotalPages > 0 && page > coll.TotalPages {
		redirURL := fmt.Sprintf("/page/%d", coll.TotalPages)
		if !app.cfg.App.SingleUser {
			redirURL = fmt.Sprintf("/%s%s%s", cr.prefix, coll.Alias, redirURL)
		}
		return impart.HTTPError{http.StatusFound, redirURL}
	}

	coll.Posts, _ = app.db.GetPosts(c, page, cr.isCollOwner, false)

	// Serve collection
	displayPage := CollectionPage{
		DisplayCollection: coll,
		StaticPage:        pageForReq(app, r),
		IsCustomDomain:    cr.isCustomDomain,
		IsWelcome:         r.FormValue("greeting") != "",
	}
	var owner *User
	if u != nil {
		displayPage.Username = u.Username
		displayPage.IsOwner = u.ID == coll.OwnerID
		if displayPage.IsOwner {
			// Add in needed information for users viewing their own collection
			owner = u
			displayPage.CanPin = true

			pubColls, err := app.db.GetPublishableCollections(owner)
			if err != nil {
				log.Error("unable to fetch collections: %v", err)
			}
			displayPage.Collections = pubColls
		}
	}
	if owner == nil {
		// Current user doesn't own collection; retrieve owner information
		owner, err = app.db.GetUserByID(coll.OwnerID)
		if err != nil {
			// Log the error and just continue
			log.Error("Error getting user for collection: %v", err)
		}
	}
	displayPage.Owner = owner
	coll.Owner = displayPage.Owner

	// Add more data
	// TODO: fix this mess of collections inside collections
	displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj)

	err = templates["collection"].ExecuteTemplate(w, "collection", displayPage)
	if err != nil {
		log.Error("Unable to render collection index: %v", err)
	}

	// Update collection view count
	go func() {
		// Don't update if owner is viewing the collection.
		if u != nil && u.ID == coll.OwnerID {
			return
		}
		// Only update for human views
		if r.Method == "HEAD" || bots.IsBot(r.UserAgent()) {
			return
		}

		_, err := app.db.Exec("UPDATE collections SET view_count = view_count + 1 WHERE id = ?", coll.ID)
		if err != nil {
			log.Error("Unable to update collections count: %v", err)
		}
	}()

	return err
}

func handleViewCollectionTag(app *app, w http.ResponseWriter, r *http.Request) error {
	vars := mux.Vars(r)
	tag := vars["tag"]

	cr := &collectionReq{}
	err := processCollectionRequest(cr, vars, w, r)
	if err != nil {
		return err
	}

	u, err := checkUserForCollection(app, cr, r, false)
	if err != nil {
		return err
	}

	page := getCollectionPage(vars)

	c, err := processCollectionPermissions(app, cr, u, w, r)
	if c == nil || err != nil {
		return err
	}

	coll := newDisplayCollection(c, cr, page)

	coll.Posts, _ = app.db.GetPostsTagged(c, tag, page, cr.isCollOwner)
	if coll.Posts != nil && len(*coll.Posts) == 0 {
		return ErrCollectionPageNotFound
	}

	// Serve collection
	displayPage := struct {
		CollectionPage
		Tag string
	}{
		CollectionPage: CollectionPage{
			DisplayCollection: coll,
			StaticPage:        pageForReq(app, r),
			IsCustomDomain:    cr.isCustomDomain,
		},
		Tag: tag,
	}
	var owner *User
	if u != nil {
		displayPage.Username = u.Username
		displayPage.IsOwner = u.ID == coll.OwnerID
		if displayPage.IsOwner {
			// Add in needed information for users viewing their own collection
			owner = u
			displayPage.CanPin = true

			pubColls, err := app.db.GetPublishableCollections(owner)
			if err != nil {
				log.Error("unable to fetch collections: %v", err)
			}
			displayPage.Collections = pubColls
		}
	}
	if owner == nil {
		// Current user doesn't own collection; retrieve owner information
		owner, err = app.db.GetUserByID(coll.OwnerID)
		if err != nil {
			// Log the error and just continue
			log.Error("Error getting user for collection: %v", err)
		}
	}
	displayPage.Owner = owner
	coll.Owner = displayPage.Owner
	// Add more data
	// TODO: fix this mess of collections inside collections
	displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj)

	err = templates["collection-tags"].ExecuteTemplate(w, "collection-tags", displayPage)
	if err != nil {
		log.Error("Unable to render collection tag page: %v", err)
	}

	return nil
}

func handleCollectionPostRedirect(app *app, w http.ResponseWriter, r *http.Request) error {
	vars := mux.Vars(r)
	slug := vars["slug"]

	cr := &collectionReq{}
	err := processCollectionRequest(cr, vars, w, r)
	if err != nil {
		return err
	}

	// Normalize the URL, redirecting user to consistent post URL
	loc := fmt.Sprintf("/%s", slug)
	if !app.cfg.App.SingleUser {
		loc = fmt.Sprintf("/%s/%s", cr.alias, slug)
	}
	return impart.HTTPError{http.StatusFound, loc}
}

func existingCollection(app *app, w http.ResponseWriter, r *http.Request) error {
	reqJSON := IsJSON(r.Header.Get("Content-Type"))
	vars := mux.Vars(r)
	collAlias := vars["alias"]
	isWeb := r.FormValue("web") == "1"

	var u *User
	if reqJSON && !isWeb {
		// Ensure an access token was given
		accessToken := r.Header.Get("Authorization")
		u = &User{}
		u.ID = app.db.GetUserID(accessToken)
		if u.ID == -1 {
			return ErrBadAccessToken
		}
	} else {
		u = getUserSession(app, r)
		if u == nil {
			return ErrNotLoggedIn
		}
	}

	if r.Method == "DELETE" {
		err := app.db.DeleteCollection(collAlias, u.ID)
		if err != nil {
			// TODO: if not HTTPError, report error to admin
			log.Error("Unable to delete collection: %s", err)
			return err
		}
		addSessionFlash(app, w, r, "Deleted your blog, "+collAlias+".", nil)
		return impart.HTTPError{Status: http.StatusNoContent}
	}

	c := SubmittedCollection{OwnerID: uint64(u.ID)}
	var err error

	if reqJSON {
		// Decode JSON request
		decoder := json.NewDecoder(r.Body)
		err = decoder.Decode(&c)
		if err != nil {
			log.Error("Couldn't parse collection update JSON request: %v\n", err)
			return ErrBadJSON
		}
	} else {
		err = r.ParseForm()
		if err != nil {
			log.Error("Couldn't parse collection update form request: %v\n", err)
			return ErrBadFormData
		}

		err = app.formDecoder.Decode(&c, r.PostForm)
		if err != nil {
			log.Error("Couldn't decode collection update form request: %v\n", err)
			return ErrBadFormData
		}
	}

	err = app.db.UpdateCollection(&c, collAlias)
	if err != nil {
		if err, ok := err.(impart.HTTPError); ok {
			if reqJSON {
				return err
			}
			addSessionFlash(app, w, r, err.Message, nil)
			return impart.HTTPError{http.StatusFound, "/me/c/" + collAlias}
		} else {
			log.Error("Couldn't update collection: %v\n", err)
			return err
		}
	}

	if reqJSON {
		return impart.WriteSuccess(w, struct {
		}{}, http.StatusOK)
	}

	addSessionFlash(app, w, r, "Blog updated!", nil)
	return impart.HTTPError{http.StatusFound, "/me/c/" + collAlias}
}

// collectionAliasFromReq takes a request and returns the collection alias
// if it can be ascertained, as well as whether or not the collection uses a
// custom domain.
func collectionAliasFromReq(r *http.Request) string {
	vars := mux.Vars(r)
	alias := vars["subdomain"]
	isSubdomain := alias != ""
	if !isSubdomain {
		// Fall back to write.as/{collection} since this isn't a custom domain
		alias = vars["collection"]
	}
	return alias
}

func handleWebCollectionUnlock(app *app, w http.ResponseWriter, r *http.Request) error {
	var readReq struct {
		Alias string `schema:"alias" json:"alias"`
		Pass  string `schema:"password" json:"password"`
		Next  string `schema:"to" json:"to"`
	}

	// Get params
	if impart.ReqJSON(r) {
		decoder := json.NewDecoder(r.Body)
		err := decoder.Decode(&readReq)
		if err != nil {
			log.Error("Couldn't parse readReq JSON request: %v\n", err)
			return ErrBadJSON
		}
	} else {
		err := r.ParseForm()
		if err != nil {
			log.Error("Couldn't parse readReq form request: %v\n", err)
			return ErrBadFormData
		}

		err = app.formDecoder.Decode(&readReq, r.PostForm)
		if err != nil {
			log.Error("Couldn't decode readReq form request: %v\n", err)
			return ErrBadFormData
		}
	}

	if readReq.Alias == "" {
		return impart.HTTPError{http.StatusBadRequest, "Need a collection `alias` to read."}
	}
	if readReq.Pass == "" {
		return impart.HTTPError{http.StatusBadRequest, "Please supply a password."}
	}

	var collHashedPass []byte
	err := app.db.QueryRow("SELECT password FROM collectionpasswords INNER JOIN collections ON id = collection_id WHERE alias = ?", readReq.Alias).Scan(&collHashedPass)
	if err != nil {
		if err == sql.ErrNoRows {
			log.Error("No collectionpassword found when trying to read collection %s", readReq.Alias)
			return impart.HTTPError{http.StatusInternalServerError, "Something went very wrong. The humans have been alerted."}
		}
		return err
	}

	if !auth.Authenticated(collHashedPass, []byte(readReq.Pass)) {
		return impart.HTTPError{http.StatusUnauthorized, "Incorrect password."}
	}

	// Success; set cookie
	session, err := app.sessionStore.Get(r, blogPassCookieName)
	if err == nil {
		session.Values[readReq.Alias] = true
		err = session.Save(r, w)
		if err != nil {
			log.Error("Didn't save unlocked blog '%s': %v", readReq.Alias, err)
		}
	}

	next := "/" + readReq.Next
	if !app.cfg.App.SingleUser {
		next = "/" + readReq.Alias + next
	}
	return impart.HTTPError{http.StatusFound, next}
}

func isAuthorizedForCollection(app *app, alias string, r *http.Request) bool {
	authd := false
	session, err := app.sessionStore.Get(r, blogPassCookieName)
	if err == nil {
		_, authd = session.Values[alias]
	}
	return authd
}
