// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com

package page

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"

	"github.com/documize/community/core/env"
	"github.com/documize/community/core/request"
	"github.com/documize/community/core/response"
	"github.com/documize/community/core/streamutil"
	"github.com/documize/community/core/uniqueid"
	"github.com/documize/community/domain"
	"github.com/documize/community/domain/link"
	"github.com/documize/community/domain/permission"
	indexer "github.com/documize/community/domain/search"
	"github.com/documize/community/domain/section/provider"
	"github.com/documize/community/model/activity"
	"github.com/documize/community/model/audit"
	dm "github.com/documize/community/model/doc"
	"github.com/documize/community/model/page"
	pm "github.com/documize/community/model/permission"
	"github.com/documize/community/model/user"
	"github.com/documize/community/model/workflow"
	htmldiff "github.com/documize/html-diff"
)

// Handler contains the runtime information such as logging and database.
type Handler struct {
	Runtime *env.Runtime
	Store   *domain.Store
	Indexer indexer.Indexer
}

// Add inserts new section into document.
func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
	method := "page.add"
	ctx := domain.GetRequestContext(r)

	if !h.Runtime.Product.License.IsValid() {
		response.WriteBadLicense(w)
		return
	}

	// check param
	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	// read payload
	defer streamutil.Close(r.Body)
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		response.WriteBadRequestError(w, method, err.Error())
		h.Runtime.Log.Error(method, err)
		return
	}

	model := new(page.NewPage)
	err = json.Unmarshal(body, &model)
	if err != nil {
		response.WriteBadRequestError(w, method, err.Error())
		h.Runtime.Log.Error(method, err)
		return
	}

	if model.Page.DocumentID != documentID {
		response.WriteBadRequestError(w, method, "documentID mismatch")
		return
	}

	if model.Meta.DocumentID != documentID {
		response.WriteBadRequestError(w, method, "documentID mismatch")
		return
	}

	// Check protection and approval process
	document, err := h.Store.Document.Get(ctx, documentID)
	if err != nil {
		response.WriteBadRequestError(w, method, err.Error())
		h.Runtime.Log.Error(method, err)
		return
	}

	// Protect locked
	if document.Protection == workflow.ProtectionLock {
		response.WriteForbiddenError(w)
		h.Runtime.Log.Info("attempted write to locked document")
		return
	}

	// Check edit permission
	if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
		response.WriteForbiddenError(w)
		return
	}

	// if document review process then we must mark page as pending
	if document.Protection == workflow.ProtectionReview {
		if model.Page.RelativeID == "" {
			model.Page.Status = workflow.ChangePendingNew
		} else {
			model.Page.Status = workflow.ChangePending
		}
	} else {
		model.Page.RelativeID = ""
		model.Page.Status = workflow.ChangePublished
	}

	pageID := uniqueid.Generate()
	model.Page.RefID = pageID
	model.Meta.PageID = pageID
	model.Meta.OrgID = ctx.OrgID   // required for Render call below
	model.Meta.UserID = ctx.UserID // required for Render call below
	model.Page.SetDefaults()
	model.Meta.SetDefaults()

	doc, err := h.Store.Document.Get(ctx, documentID)
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	ctx.Transaction, err = h.Runtime.Db.Beginx()
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	output, ok := provider.Render(model.Page.ContentType, provider.NewContext(model.Meta.OrgID, model.Meta.UserID, ctx), model.Meta.Config, model.Meta.RawBody)
	if !ok {
		h.Runtime.Log.Info("provider.Render could not find: " + model.Page.ContentType)
	}

	model.Page.Body = output

	err = h.Store.Page.Add(ctx, *model)
	if err != nil {
		ctx.Transaction.Rollback()
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	if len(model.Page.BlockID) > 0 {
		h.Store.Block.IncrementUsage(ctx, model.Page.BlockID)
	}

	// Draft actions are not logged
	if doc.Lifecycle == workflow.LifecycleLive {
		h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
			LabelID:      doc.LabelID,
			DocumentID:   model.Page.DocumentID,
			PageID:       model.Page.RefID,
			SourceType:   activity.SourceTypePage,
			ActivityType: activity.TypeCreated})
	}

	ctx.Transaction.Commit()

	h.Store.Audit.Record(ctx, audit.EventTypeSectionAdd)

	np, _ := h.Store.Page.Get(ctx, pageID)

	if doc.Lifecycle == workflow.LifecycleLive {
		go h.Indexer.IndexContent(ctx, np)
	} else {
		go h.Indexer.DeleteDocument(ctx, doc.RefID)
	}

	response.WriteJSON(w, np)
}

// GetPage gets specified page for document.
func (h *Handler) GetPage(w http.ResponseWriter, r *http.Request) {
	method := "page.GetPage"
	ctx := domain.GetRequestContext(r)

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	pageID := request.Param(r, "pageID")
	if len(pageID) == 0 {
		response.WriteMissingDataError(w, method, "pageID")
		return
	}

	if !permission.CanViewDocument(ctx, *h.Store, documentID) {
		response.WriteForbiddenError(w)
		return
	}

	page, err := h.Store.Page.Get(ctx, pageID)
	if err == sql.ErrNoRows {
		response.WriteNotFoundError(w, method, documentID)
		return
	}
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	if page.DocumentID != documentID {
		response.WriteBadRequestError(w, method, "documentID mismatch")
		return
	}

	response.WriteJSON(w, page)
}

// GetPages gets all pages for document.
func (h *Handler) GetPages(w http.ResponseWriter, r *http.Request) {
	method := "page.GetPages"
	ctx := domain.GetRequestContext(r)

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	if !permission.CanViewDocument(ctx, *h.Store, documentID) {
		response.WriteForbiddenError(w)
		return
	}

	var pages []page.Page
	var err error
	content := request.Query(r, "content")

	if len(content) > 0 {
		pages, err = h.Store.Page.GetPagesWithoutContent(ctx, documentID)
	} else {
		pages, err = h.Store.Page.GetPages(ctx, documentID)
	}

	if len(pages) == 0 {
		pages = []page.Page{}
	}

	page.Numberize(pages)

	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
	}

	response.WriteJSON(w, pages)
}

// GetMeta gets page meta data for specified document page.
func (h *Handler) GetMeta(w http.ResponseWriter, r *http.Request) {
	method := "page.meta"
	ctx := domain.GetRequestContext(r)

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	pageID := request.Param(r, "pageID")
	if len(pageID) == 0 {
		response.WriteMissingDataError(w, method, "pageID")
		return
	}

	if !permission.CanViewDocument(ctx, *h.Store, documentID) {
		response.WriteForbiddenError(w)
		return
	}

	meta, err := h.Store.Page.GetPageMeta(ctx, pageID)
	if err == sql.ErrNoRows {
		h.Runtime.Log.Info(method + " no record")
		meta = page.Meta{}
		response.WriteJSON(w, meta)
		return
	}
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}
	if meta.DocumentID != documentID {
		response.WriteBadRequestError(w, method, "documentID mismatch")
		h.Runtime.Log.Error(method, err)
		return
	}

	response.WriteJSON(w, meta)
}

// Update will persist changed page and note the fact
// that this is a new revision. If the page is the first in a document
// then the corresponding document title will also be changed.
// Draft documents do not get revision entry.
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
	method := "page.update"
	ctx := domain.GetRequestContext(r)

	if !h.Runtime.Product.License.IsValid() {
		response.WriteBadLicense(w)
		return
	}

	// Check params
	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	pageID := request.Param(r, "pageID")
	if len(pageID) == 0 {
		response.WriteMissingDataError(w, method, "pageID")
		return
	}

	// Read payload
	defer streamutil.Close(r.Body)
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		response.WriteBadRequestError(w, method, "Bad request body")
		h.Runtime.Log.Error(method, err)
		return
	}

	model := new(page.NewPage)
	err = json.Unmarshal(body, &model)
	if err != nil {
		response.WriteBadRequestError(w, method, err.Error())
		h.Runtime.Log.Error(method, err)
		return
	}

	if model.Page.RefID != pageID || model.Page.DocumentID != documentID {
		response.WriteBadRequestError(w, method, err.Error())
		return
	}

	// Check protection and approval process
	doc, err := h.Store.Document.Get(ctx, documentID)
	if err != nil {
		response.WriteBadRequestError(w, method, err.Error())
		h.Runtime.Log.Error(method, err)
		return
	}

	if doc.Protection == workflow.ProtectionLock {
		response.WriteForbiddenError(w)
		h.Runtime.Log.Info("attempted write to locked document")
		return
	}

	// Check edit permission
	if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
		response.WriteForbiddenError(w)
		return
	}

	ctx.Transaction, err = h.Runtime.Db.Beginx()
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	model.Page.SetDefaults()
	model.Meta.SetDefaults()

	oldPageMeta, err := h.Store.Page.GetPageMeta(ctx, pageID)
	if err != nil {
		ctx.Transaction.Rollback()
		response.WriteBadRequestError(w, method, err.Error())
		h.Runtime.Log.Error(method, err)
		return
	}

	output, ok := provider.Render(model.Page.ContentType, provider.NewContext(model.Meta.OrgID, oldPageMeta.UserID, ctx), model.Meta.Config, model.Meta.RawBody)
	if !ok {
		h.Runtime.Log.Info("provider.Render could not find: " + model.Page.ContentType)
	}

	model.Page.Body = output
	refID := uniqueid.Generate()
	skipRevision := false
	skipRevision, err = strconv.ParseBool(request.Query(r, "r"))

	// We don't track revisions for non-published pages
	if model.Page.Status != workflow.ChangePublished {
		skipRevision = true
	}

	// We only track revisions for live documents
	if doc.Lifecycle != workflow.LifecycleLive {
		skipRevision = true
	}

	err = h.Store.Page.Update(ctx, model.Page, refID, ctx.UserID, skipRevision)
	if err != nil {
		ctx.Transaction.Rollback()
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	err = h.Store.Page.UpdateMeta(ctx, model.Meta, true) // change the UserID to the current one
	if err != nil {
		ctx.Transaction.Rollback()
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	// Draft edits are not logged
	if doc.Lifecycle == workflow.LifecycleLive {
		h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
			LabelID:      doc.LabelID,
			DocumentID:   model.Page.DocumentID,
			PageID:       model.Page.RefID,
			SourceType:   activity.SourceTypePage,
			ActivityType: activity.TypeEdited})
	}

	h.Store.Audit.Record(ctx, audit.EventTypeSectionUpdate)

	// find any content links in the HTML
	links := link.GetContentLinks(model.Page.Body)

	// get a copy of previously saved links
	previousLinks, _ := h.Store.Link.GetPageLinks(ctx, model.Page.DocumentID, model.Page.RefID)

	// delete previous content links for this page
	_, _ = h.Store.Link.DeleteSourcePageLinks(ctx, model.Page.RefID)

	// save latest content links for this page
	for _, link := range links {
		link.Orphan = false
		link.OrgID = ctx.OrgID
		link.UserID = ctx.UserID
		link.SourceDocumentID = model.Page.DocumentID
		link.SourcePageID = model.Page.RefID

		if link.LinkType == "document" {
			link.TargetID = ""
		}

		// We check if there was a previously saved version of this link.
		// If we find one, we carry forward the orphan flag.
		for _, p := range previousLinks {
			if link.TargetID == p.TargetID && link.LinkType == p.LinkType {
				link.Orphan = p.Orphan
				break
			}
		}

		// save
		err := h.Store.Link.Add(ctx, link)
		if err != nil {
			h.Runtime.Log.Error(fmt.Sprintf("Unable to insert content links for page %s", model.Page.RefID), err)
		}
	}

	ctx.Transaction.Commit()

	if doc.Lifecycle == workflow.LifecycleLive {
		go h.Indexer.IndexContent(ctx, model.Page)
	} else {
		go h.Indexer.DeleteDocument(ctx, doc.RefID)
	}

	updatedPage, err := h.Store.Page.Get(ctx, pageID)

	response.WriteJSON(w, updatedPage)
}

// Delete a page.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
	method := "page.delete"
	ctx := domain.GetRequestContext(r)

	if !h.Runtime.Product.License.IsValid() {
		response.WriteBadLicense(w)
		return
	}

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}
	pageID := request.Param(r, "pageID")
	if len(pageID) == 0 {
		response.WriteMissingDataError(w, method, "pageID")
		return
	}

	doc, err := h.Store.Document.Get(ctx, documentID)
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	p, err := h.Store.Page.Get(ctx, pageID)
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	// you can delete your own pending page
	ownPending := false
	if (p.Status == workflow.ChangePending || p.Status == workflow.ChangePendingNew) && p.UserID == ctx.UserID {
		ownPending = true
	}

	// if not own page then check permission
	if !ownPending {
		ok, _ := h.workflowPermitsChange(doc, ctx)
		if !ok {
			response.WriteForbiddenError(w)
			h.Runtime.Log.Info("attempted delete section on locked document")
			return
		}
	}

	ctx.Transaction, err = h.Runtime.Db.Beginx()
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	if len(p.BlockID) > 0 {
		h.Store.Block.DecrementUsage(ctx, p.BlockID)
	}

	_, err = h.Store.Page.Delete(ctx, documentID, pageID)
	if err != nil {
		ctx.Transaction.Rollback()
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	// Draft actions are not logged
	if doc.Lifecycle == workflow.LifecycleLive {
		h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
			LabelID:      doc.LabelID,
			DocumentID:   documentID,
			PageID:       pageID,
			SourceType:   activity.SourceTypePage,
			ActivityType: activity.TypeDeleted})
	}

	go h.Indexer.DeleteContent(ctx, pageID)

	h.Store.Link.DeleteSourcePageLinks(ctx, pageID)

	h.Store.Link.MarkOrphanPageLink(ctx, pageID)

	h.Store.Page.DeletePageRevisions(ctx, pageID)

	ctx.Transaction.Commit()

	h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)

	// Re-level all pages in document
	h.LevelizeDocument(ctx, documentID)

	response.WriteEmpty(w)
}

// DeletePages batch deletes pages.
func (h *Handler) DeletePages(w http.ResponseWriter, r *http.Request) {
	method := "page.delete.pages"
	ctx := domain.GetRequestContext(r)

	if !h.Runtime.Product.License.IsValid() {
		response.WriteBadLicense(w)
		return
	}

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	defer streamutil.Close(r.Body)
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		response.WriteBadRequestError(w, method, "Bad body")
		return
	}

	model := new([]page.LevelRequest)
	err = json.Unmarshal(body, &model)
	if err != nil {
		response.WriteBadRequestError(w, method, "JSON marshal")
		return
	}

	doc, err := h.Store.Document.Get(ctx, documentID)
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	ctx.Transaction, err = h.Runtime.Db.Beginx()
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	for _, page := range *model {
		pageData, err := h.Store.Page.Get(ctx, page.PageID)
		if err != nil {
			ctx.Transaction.Rollback()
			response.WriteServerError(w, method, err)
			h.Runtime.Log.Error(method, err)
			return
		}

		ownPending := false
		if (pageData.Status == workflow.ChangePending || pageData.Status == workflow.ChangePendingNew) && pageData.UserID == ctx.UserID {
			ownPending = true
		}

		// if not own page then check permission
		if !ownPending {
			ok, _ := h.workflowPermitsChange(doc, ctx)
			if !ok {
				ctx.Transaction.Rollback()
				response.WriteForbiddenError(w)
				h.Runtime.Log.Info("attempted delete section on locked document")
				return
			}
		}
		if len(pageData.BlockID) > 0 {
			h.Store.Block.DecrementUsage(ctx, pageData.BlockID)
		}

		_, err = h.Store.Page.Delete(ctx, documentID, page.PageID)
		if err != nil {
			ctx.Transaction.Rollback()
			response.WriteServerError(w, method, err)
			h.Runtime.Log.Error(method, err)
			return
		}

		go h.Indexer.DeleteContent(ctx, page.PageID)

		h.Store.Link.DeleteSourcePageLinks(ctx, page.PageID)

		h.Store.Link.MarkOrphanPageLink(ctx, page.PageID)

		h.Store.Page.DeletePageRevisions(ctx, page.PageID)

		// Draft actions are not logged
		if doc.Lifecycle == workflow.LifecycleLive {
			h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
				LabelID:      doc.LabelID,
				DocumentID:   documentID,
				PageID:       page.PageID,
				SourceType:   activity.SourceTypePage,
				ActivityType: activity.TypeDeleted})
		}
	}

	ctx.Transaction.Commit()

	h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)

	// Re-level all pages in document
	h.LevelizeDocument(ctx, documentID)

	response.WriteEmpty(w)
}

//**************************************************
// Table of Contents
//**************************************************

// ChangePageSequence will swap page sequence for a given number of pages.
func (h *Handler) ChangePageSequence(w http.ResponseWriter, r *http.Request) {
	method := "page.sequence"
	ctx := domain.GetRequestContext(r)

	if !h.Runtime.Product.License.IsValid() {
		response.WriteBadLicense(w)
		return
	}

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	doc, err := h.Store.Document.Get(ctx, documentID)
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	ok, err := h.workflowPermitsChange(doc, ctx)
	if !ok {
		response.WriteForbiddenError(w)
		h.Runtime.Log.Info("attempted to chaneg page sequence on protected document")
		return
	}

	defer streamutil.Close(r.Body)
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		response.WriteBadRequestError(w, method, err.Error())
		h.Runtime.Log.Error(method, err)
		return
	}

	model := new([]page.SequenceRequest)
	err = json.Unmarshal(body, &model)
	if err != nil {
		response.WriteBadRequestError(w, method, err.Error())
		h.Runtime.Log.Error(method, err)
		return
	}

	ctx.Transaction, err = h.Runtime.Db.Beginx()
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	for _, p := range *model {
		err = h.Store.Page.UpdateSequence(ctx, documentID, p.PageID, p.Sequence)
		if err != nil {
			ctx.Transaction.Rollback()
			response.WriteServerError(w, method, err)
			h.Runtime.Log.Error(method, err)
			return
		}
	}

	ctx.Transaction.Commit()

	h.Store.Audit.Record(ctx, audit.EventTypeSectionResequence)

	response.WriteEmpty(w)
}

// ChangePageLevel handles page indent/outdent changes.
func (h *Handler) ChangePageLevel(w http.ResponseWriter, r *http.Request) {
	method := "page.level"
	ctx := domain.GetRequestContext(r)

	if !h.Runtime.Product.License.IsValid() {
		response.WriteBadLicense(w)
		return
	}

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}
	doc, err := h.Store.Document.Get(ctx, documentID)
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	ok, err := h.workflowPermitsChange(doc, ctx)
	if !ok {
		response.WriteForbiddenError(w)
		h.Runtime.Log.Info("attempted to chaneg page level on protected document")
		return
	}

	defer streamutil.Close(r.Body)
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		response.WriteBadRequestError(w, method, err.Error())
		h.Runtime.Log.Error(method, err)
		return
	}

	model := new([]page.LevelRequest)
	err = json.Unmarshal(body, &model)
	if err != nil {
		response.WriteBadRequestError(w, method, err.Error())
		h.Runtime.Log.Error(method, err)
		return
	}

	ctx.Transaction, err = h.Runtime.Db.Beginx()
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	for _, p := range *model {
		err = h.Store.Page.UpdateLevel(ctx, documentID, p.PageID, p.Level)
		if err != nil {
			ctx.Transaction.Rollback()
			response.WriteServerError(w, method, err)
			h.Runtime.Log.Error(method, err)
			return
		}
	}

	ctx.Transaction.Commit()

	h.Store.Audit.Record(ctx, audit.EventTypeSectionResequence)

	response.WriteEmpty(w)
}

//**************************************************
// Copy Move Page
//**************************************************

// Copy copies page to either same or different document.
func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
	method := "page.targets"
	ctx := domain.GetRequestContext(r)

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	pageID := request.Param(r, "pageID")
	if len(pageID) == 0 {
		response.WriteMissingDataError(w, method, "pageID")
		return
	}

	targetID := request.Param(r, "targetID")
	if len(targetID) == 0 {
		response.WriteMissingDataError(w, method, "targetID")
		return
	}

	// permission
	if !permission.CanViewDocument(ctx, *h.Store, documentID) {
		response.WriteForbiddenError(w)
		return
	}

	// fetch data
	doc, err := h.Store.Document.Get(ctx, documentID)
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	// workflow check
	if doc.Protection == workflow.ProtectionLock || doc.Protection == workflow.ProtectionReview {
		response.WriteForbiddenError(w)
		return
	}

	p, err := h.Store.Page.Get(ctx, pageID)
	if err == sql.ErrNoRows {
		response.WriteNotFoundError(w, method, documentID)
		h.Runtime.Log.Error(method, err)
		return
	}
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	pageMeta, err := h.Store.Page.GetPageMeta(ctx, pageID)
	if err == sql.ErrNoRows {
		response.WriteNotFoundError(w, method, documentID)
		h.Runtime.Log.Error(method, err)
		return
	}
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	newPageID := uniqueid.Generate()
	p.RefID = newPageID
	p.Level = 1
	p.Sequence = 0
	p.DocumentID = targetID
	p.UserID = ctx.UserID
	pageMeta.DocumentID = targetID
	pageMeta.PageID = newPageID
	pageMeta.UserID = ctx.UserID

	model := new(page.NewPage)
	model.Meta = pageMeta
	model.Page = p

	ctx.Transaction, err = h.Runtime.Db.Beginx()
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	err = h.Store.Page.Add(ctx, *model)
	if err != nil {
		ctx.Transaction.Rollback()
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	if len(model.Page.BlockID) > 0 {
		h.Store.Block.IncrementUsage(ctx, model.Page.BlockID)
	}

	// Log t actions are not logged
	if doc.Lifecycle == workflow.LifecycleLive {
		h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
			LabelID:      doc.LabelID,
			DocumentID:   targetID,
			PageID:       newPageID,
			SourceType:   activity.SourceTypePage,
			ActivityType: activity.TypeCreated})
	}

	ctx.Transaction.Commit()

	h.Store.Audit.Record(ctx, audit.EventTypeSectionCopy)

	np, _ := h.Store.Page.Get(ctx, pageID)

	response.WriteJSON(w, np)
}

//**************************************************
// Revisions
//**************************************************

// GetDocumentRevisions returns all changes for a document.
func (h *Handler) GetDocumentRevisions(w http.ResponseWriter, r *http.Request) {
	method := "page.document.revisions"
	ctx := domain.GetRequestContext(r)

	if !h.Runtime.Product.License.IsValid() {
		response.WriteBadLicense(w)
		return
	}

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	if !permission.CanViewDocument(ctx, *h.Store, documentID) {
		response.WriteForbiddenError(w)
		return
	}

	revisions, _ := h.Store.Page.GetDocumentRevisions(ctx, documentID)
	if len(revisions) == 0 {
		revisions = []page.Revision{}
	}

	h.Store.Audit.Record(ctx, audit.EventTypeDocumentRevisions)

	response.WriteJSON(w, revisions)
}

// GetRevisions returns all changes for a given page.
func (h *Handler) GetRevisions(w http.ResponseWriter, r *http.Request) {
	method := "page.revisions"
	ctx := domain.GetRequestContext(r)

	if !h.Runtime.Product.License.IsValid() {
		response.WriteBadLicense(w)
		return
	}

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	if !permission.CanViewDocument(ctx, *h.Store, documentID) {
		response.WriteForbiddenError(w)
		return
	}

	pageID := request.Param(r, "pageID")
	if len(pageID) == 0 {
		response.WriteMissingDataError(w, method, "pageID")
		return
	}

	revisions, _ := h.Store.Page.GetPageRevisions(ctx, pageID)
	if len(revisions) == 0 {
		revisions = []page.Revision{}
	}

	response.WriteJSON(w, revisions)
}

// GetDiff returns HTML diff between two revisions of a given page.
func (h *Handler) GetDiff(w http.ResponseWriter, r *http.Request) {
	method := "page.diff"
	ctx := domain.GetRequestContext(r)

	if !h.Runtime.Product.License.IsValid() {
		response.WriteBadLicense(w)
		return
	}

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	pageID := request.Param(r, "pageID")
	if len(pageID) == 0 {
		response.WriteMissingDataError(w, method, "pageID")
		return
	}

	revisionID := request.Param(r, "revisionID")
	if len(revisionID) == 0 {
		response.WriteMissingDataError(w, method, "revisionID")
		return
	}

	if !permission.CanViewDocument(ctx, *h.Store, documentID) {
		response.WriteForbiddenError(w)
		return
	}

	p, err := h.Store.Page.Get(ctx, pageID)
	if err == sql.ErrNoRows {
		response.WriteNotFoundError(w, method, pageID)
		h.Runtime.Log.Error(method, err)
		return
	}
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	revision, _ := h.Store.Page.GetPageRevision(ctx, revisionID)

	latestHTML := p.Body
	previousHTML := revision.Body
	var result []byte

	var cfg = &htmldiff.Config{
		Granularity:  5,
		DeletedSpan:  []htmldiff.Attribute{{Key: "style", Val: "background-color: palegreen;"}},
		InsertedSpan: []htmldiff.Attribute{{Key: "style", Val: "background-color: lightpink; text-decoration: line-through;"}},
		ReplacedSpan: []htmldiff.Attribute{{Key: "style", Val: "background-color: lightskyblue;"}},
		CleanTags:    []string{"documize"},
	}
	// InsertedSpan: []htmldiff.Attribute{{Key: "style", Val: "background-color: palegreen;"}},
	// DeletedSpan:  []htmldiff.Attribute{{Key: "style", Val: "background-color: lightpink; text-decoration: line-through;"}},
	// ReplacedSpan: []htmldiff.Attribute{{Key: "style", Val: "background-color: lightskyblue;"}},

	// res, err := cfg.HTMLdiff([]string{latestHTML, previousHTML})
	res, err := cfg.HTMLdiff([]string{previousHTML, latestHTML})
	if err != nil {
		response.WriteServerError(w, method, err)
		return
	}

	result = []byte(res[0])
	w.Write(result)
}

// Rollback rolls back to a specific page revision.
func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) {
	method := "page.rollback"
	ctx := domain.GetRequestContext(r)

	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	pageID := request.Param(r, "pageID")
	if len(pageID) == 0 {
		response.WriteMissingDataError(w, method, "pageID")
		return
	}

	revisionID := request.Param(r, "revisionID")
	if len(revisionID) == 0 {
		response.WriteMissingDataError(w, method, "revisionID")
		return
	}

	doc, err := h.Store.Document.Get(ctx, documentID)
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	ok, err := h.workflowPermitsChange(doc, ctx)
	if !ok {
		response.WriteForbiddenError(w)
		h.Runtime.Log.Info("attempted to chaneg page sequence on protected document")
		return
	}

	ctx.Transaction, err = h.Runtime.Db.Beginx()
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	p, err := h.Store.Page.Get(ctx, pageID)
	if err != nil {
		ctx.Transaction.Rollback()
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	meta, err := h.Store.Page.GetPageMeta(ctx, pageID)
	if err != nil {
		ctx.Transaction.Rollback()
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	revision, err := h.Store.Page.GetPageRevision(ctx, revisionID)
	if err != nil {
		ctx.Transaction.Rollback()
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	// roll back page
	p.Body = revision.Body
	refID := uniqueid.Generate()

	err = h.Store.Page.Update(ctx, p, refID, ctx.UserID, false)
	if err != nil {
		ctx.Transaction.Rollback()
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	// roll back page meta
	meta.Config = revision.Config
	meta.RawBody = revision.RawBody

	err = h.Store.Page.UpdateMeta(ctx, meta, false)
	if err != nil {
		ctx.Transaction.Rollback()
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	// Draft actions are not logged
	if doc.Lifecycle == workflow.LifecycleLive {
		h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
			LabelID:      doc.LabelID,
			DocumentID:   p.DocumentID,
			PageID:       p.RefID,
			SourceType:   activity.SourceTypePage,
			ActivityType: activity.TypeReverted})
	}

	ctx.Transaction.Commit()

	h.Store.Audit.Record(ctx, audit.EventTypeSectionRollback)

	response.WriteJSON(w, p)
}

//**************************************************
// Bulk data fetching (reduce network traffic)
//**************************************************

// FetchPages returns all page data for given document: page, meta data, pending changes.
func (h *Handler) FetchPages(w http.ResponseWriter, r *http.Request) {
	method := "page.FetchPages"
	ctx := domain.GetRequestContext(r)
	model := []page.BulkRequest{}

	// check params
	documentID := request.Param(r, "documentID")
	if len(documentID) == 0 {
		response.WriteMissingDataError(w, method, "documentID")
		return
	}

	// Who referred user this document (e.g. search page).
	source := request.Query(r, "source")

	doc, err := h.Store.Document.Get(ctx, documentID)
	if err != nil {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}

	// published pages and new pages awaiting approval
	pages, err := h.Store.Page.GetPages(ctx, documentID)
	if err != nil && err != sql.ErrNoRows {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}
	if len(pages) == 0 {
		pages = []page.Page{}
	}

	// unpublished pages
	unpublished, err := h.Store.Page.GetUnpublishedPages(ctx, documentID)
	if err != nil && err != sql.ErrNoRows {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}
	if len(unpublished) == 0 {
		unpublished = []page.Page{}
	}

	// meta for all pages
	meta, err := h.Store.Page.GetDocumentPageMeta(ctx, documentID, false)
	if err != nil && err != sql.ErrNoRows {
		response.WriteServerError(w, method, err)
		h.Runtime.Log.Error(method, err)
		return
	}
	if len(meta) == 0 {
		meta = []page.Meta{}
	}

	// permissions
	perms, err := h.Store.Permission.GetUserSpacePermissions(ctx, doc.LabelID)
	if err != nil && err != sql.ErrNoRows {
		response.WriteServerError(w, method, err)
		return
	}
	if len(perms) == 0 {
		perms = []pm.Permission{}
	}
	permissions := pm.DecodeUserPermissions(perms)

	roles, err := h.Store.Permission.GetUserDocumentPermissions(ctx, doc.RefID)
	if err != nil && err != sql.ErrNoRows {
		response.WriteServerError(w, method, err)
		return
	}
	if len(roles) == 0 {
		roles = []pm.Permission{}
	}
	docRoles := pm.DecodeUserDocumentPermissions(roles)

	// check document view permissions
	if !permissions.SpaceView && !permissions.SpaceManage && !permissions.SpaceOwner {
		response.WriteForbiddenError(w)
		return
	}

	// process published pages
	for _, p := range pages {
		// only send back pages that user can see
		process := false
		forcePending := false

		if process == false && p.Status == workflow.ChangePublished {
			process = true
		}
		if process == false && p.Status == workflow.ChangePendingNew && p.RelativeID == "" && p.UserID == ctx.UserID {
			process = true
			forcePending = true // user has newly created page which should be treated as pending
		}
		if process == false && p.Status == workflow.ChangeUnderReview && p.RelativeID == "" && p.UserID == ctx.UserID {
			process = true
			forcePending = true // user has newly created page which should be treated as pending
		}
		if process == false && p.Status == workflow.ChangeUnderReview && p.RelativeID == "" && (permissions.DocumentApprove || docRoles.DocumentRoleApprove) {
			process = true
			forcePending = true // user has newly created page which should be treated as pending
		}

		if process {
			d := page.BulkRequest{}
			d.ID = fmt.Sprintf("container-%s", p.RefID)
			d.Page = p

			for _, m := range meta {
				if p.RefID == m.PageID {
					d.Meta = m
					break
				}
			}

			d.Pending = []page.PendingPage{}

			// process pending pages
			for _, up := range unpublished {
				if up.RelativeID == p.RefID {
					ud := page.PendingPage{}
					ud.Page = up

					for _, m := range meta {
						if up.RefID == m.PageID {
							ud.Meta = m
							break
						}
					}

					owner, err := h.Store.User.Get(ctx, up.UserID)
					if err == nil {
						ud.Owner = owner.Fullname()
					}

					d.Pending = append(d.Pending, ud)
				}
			}

			// Handle situation where we need approval, and user has created new page
			if forcePending && len(d.Pending) == 0 && doc.Protection == workflow.ProtectionReview {
				ud := page.PendingPage{}
				ud.Page = d.Page
				ud.Meta = d.Meta

				owner, err := h.Store.User.Get(ctx, d.Page.UserID)
				if err == nil {
					ud.Owner = owner.Fullname()
				}

				d.Pending = append(d.Pending, ud)
			}

			model = append(model, d)
		}
	}

	// Attach numbers to pages, 1.1, 2.1.1 etc.
	t := []page.Page{}
	for _, i := range model {
		t = append(t, i.Page)
	}
	page.Numberize(t)
	for i, j := range t {
		model[i].Page = j
		// pending pages get same numbering
		for k := range model[i].Pending {
			model[i].Pending[k].Page.Numbering = j.Numbering
		}
	}

	// If we have source, record document access via source.
	if len(source) > 0 {
		ctx.Transaction, err = h.Runtime.Db.Beginx()
		if err != nil {
			h.Runtime.Log.Error(method, err)
		} else {
			err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
				LabelID:      doc.LabelID,
				DocumentID:   doc.RefID,
				Metadata:     source,                    // deliberate
				SourceType:   activity.SourceTypeSearch, // deliberate
				ActivityType: activity.TypeRead})

			if err != nil {
				ctx.Transaction.Rollback()
				h.Runtime.Log.Error(method, err)
			}

			ctx.Transaction.Commit()
		}
	}

	// deliver payload
	response.WriteJSON(w, model)
}

func (h *Handler) workflowPermitsChange(doc dm.Document, ctx domain.RequestContext) (ok bool, err error) {
	if doc.Protection == workflow.ProtectionNone {
		if !permission.CanChangeDocument(ctx, *h.Store, doc.RefID) {
			h.Runtime.Log.Info("attempted forbidden action on document")
			return false, nil
		}

		return true, nil
	}

	// If locked document then no can do
	if doc.Protection == workflow.ProtectionLock {
		h.Runtime.Log.Info("attempted action on locked document")
		return false, err
	}

	// If approval workflow then only approvers can delete page
	if doc.Protection == workflow.ProtectionReview {
		approvers, err := permission.GetUsersWithDocumentPermission(ctx, *h.Store, doc.LabelID, doc.RefID, pm.DocumentApprove)
		if err != nil {
			h.Runtime.Log.Error("workflowAllowsChange", err)
			return false, err
		}

		if user.Exists(approvers, ctx.UserID) {
			h.Runtime.Log.Info("attempted action on document when not approver")
			return true, nil
		}

		return false, nil
	}

	return true, nil
}
