// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package repo

import (
	"bytes"
	"html"
	"net/http"
	"strings"

	"code.gitea.io/gitea/models/avatars"
	issues_model "code.gitea.io/gitea/models/issues"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"
	"code.gitea.io/gitea/modules/templates"
	"code.gitea.io/gitea/modules/timeutil"
	"code.gitea.io/gitea/services/context"

	"github.com/sergi/go-diff/diffmatchpatch"
)

// GetContentHistoryOverview get overview
func GetContentHistoryOverview(ctx *context.Context) {
	issue := GetActionIssue(ctx)
	if ctx.Written() {
		return
	}

	editedHistoryCountMap, _ := issues_model.QueryIssueContentHistoryEditedCountMap(ctx, issue.ID)
	ctx.JSON(http.StatusOK, map[string]any{
		"i18n": map[string]any{
			"textEdited":                   ctx.Tr("repo.issues.content_history.edited"),
			"textDeleteFromHistory":        ctx.Tr("repo.issues.content_history.delete_from_history"),
			"textDeleteFromHistoryConfirm": ctx.Tr("repo.issues.content_history.delete_from_history_confirm"),
			"textOptions":                  ctx.Tr("repo.issues.content_history.options"),
		},
		"editedHistoryCountMap": editedHistoryCountMap,
	})
}

// GetContentHistoryList  get list
func GetContentHistoryList(ctx *context.Context) {
	issue := GetActionIssue(ctx)
	if ctx.Written() {
		return
	}

	commentID := ctx.FormInt64("comment_id")
	items, _ := issues_model.FetchIssueContentHistoryList(ctx, issue.ID, commentID)

	// render history list to HTML for frontend dropdown items: (name, value)
	// name is HTML of "avatar + userName + userAction + timeSince"
	// value is historyId
	var results []map[string]any
	for _, item := range items {
		var actionText string
		if item.IsDeleted {
			actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
			actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
		} else if item.IsFirstCreated {
			actionText = ctx.Locale.TrString("repo.issues.content_history.created")
		} else {
			actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
		}

		username := item.UserName
		if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" {
			username = strings.TrimSpace(item.UserFullName)
		}

		src := html.EscapeString(item.UserAvatarLink)
		class := avatars.DefaultAvatarClass + " tw-mr-2"
		name := html.EscapeString(username)
		avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
		timeSinceText := string(timeutil.TimeSinceUnix(item.EditedUnix, ctx.Locale))

		results = append(results, map[string]any{
			"name":  avatarHTML + "<strong>" + name + "</strong> " + actionText + " " + timeSinceText,
			"value": item.HistoryID,
		})
	}

	ctx.JSON(http.StatusOK, map[string]any{
		"results": results,
	})
}

// canSoftDeleteContentHistory checks whether current user can soft-delete a history revision
// Admins or owners can always delete history revisions. Normal users can only delete own history revisions.
func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue, comment *issues_model.Comment,
	history *issues_model.ContentHistory,
) (canSoftDelete bool) {
	// CanWrite means the doer can manage the issue/PR list
	if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
		canSoftDelete = true
	} else if ctx.Doer != nil {
		// for read-only users, they could still post issues or comments,
		// they should be able to delete the history related to their own issue/comment, a case is:
		// 1. the user posts some sensitive data
		// 2. then the repo owner edits the post but didn't remove the sensitive data
		// 3. the poster wants to delete the edited history revision
		if comment == nil {
			// the issue poster or the history poster can soft-delete
			canSoftDelete = ctx.Doer.ID == issue.PosterID || ctx.Doer.ID == history.PosterID
			canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
		} else {
			// the comment poster or the history poster can soft-delete
			canSoftDelete = ctx.Doer.ID == comment.PosterID || ctx.Doer.ID == history.PosterID
			canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
			canSoftDelete = canSoftDelete && (history.CommentID == comment.ID)
		}
	}
	return canSoftDelete
}

// GetContentHistoryDetail get detail
func GetContentHistoryDetail(ctx *context.Context) {
	issue := GetActionIssue(ctx)
	if ctx.Written() {
		return
	}

	historyID := ctx.FormInt64("history_id")
	history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, issue.ID, historyID)
	if err != nil {
		ctx.JSON(http.StatusNotFound, map[string]any{
			"message": "Can not find the content history",
		})
		return
	}

	// get the related comment if this history revision is for a comment, otherwise the history revision is for an issue.
	var comment *issues_model.Comment
	if history.CommentID != 0 {
		var err error
		if comment, err = issues_model.GetCommentByID(ctx, history.CommentID); err != nil {
			log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
			return
		}
	}

	// get the previous history revision (if exists)
	var prevHistoryID int64
	var prevHistoryContentText string
	if prevHistory != nil {
		prevHistoryID = prevHistory.ID
		prevHistoryContentText = prevHistory.ContentText
	}

	// compare the current history revision with the previous one
	dmp := diffmatchpatch.New()
	// `checklines=false` makes better diff result
	diff := dmp.DiffMain(prevHistoryContentText, history.ContentText, false)
	diff = dmp.DiffCleanupEfficiency(diff)

	// use chroma to render the diff html
	diffHTMLBuf := bytes.Buffer{}
	diffHTMLBuf.WriteString("<pre class='chroma' style='tab-size: 4'>")
	for _, it := range diff {
		if it.Type == diffmatchpatch.DiffInsert {
			diffHTMLBuf.WriteString("<span class='gi'>")
			diffHTMLBuf.WriteString(html.EscapeString(it.Text))
			diffHTMLBuf.WriteString("</span>")
		} else if it.Type == diffmatchpatch.DiffDelete {
			diffHTMLBuf.WriteString("<span class='gd'>")
			diffHTMLBuf.WriteString(html.EscapeString(it.Text))
			diffHTMLBuf.WriteString("</span>")
		} else {
			diffHTMLBuf.WriteString(html.EscapeString(it.Text))
		}
	}
	diffHTMLBuf.WriteString("</pre>")

	ctx.JSON(http.StatusOK, map[string]any{
		"canSoftDelete": canSoftDeleteContentHistory(ctx, issue, comment, history),
		"historyId":     historyID,
		"prevHistoryId": prevHistoryID,
		"diffHtml":      diffHTMLBuf.String(),
	})
}

// SoftDeleteContentHistory soft delete
func SoftDeleteContentHistory(ctx *context.Context) {
	issue := GetActionIssue(ctx)
	if ctx.Written() {
		return
	}
	if ctx.Doer == nil {
		ctx.NotFound("Require SignIn", nil)
		return
	}

	commentID := ctx.FormInt64("comment_id")
	historyID := ctx.FormInt64("history_id")

	var comment *issues_model.Comment
	var history *issues_model.ContentHistory
	var err error

	if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
		log.Error("can not get issue content history %v. err=%v", historyID, err)
		return
	}
	if history.IssueID != issue.ID {
		ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
		return
	}
	if commentID != 0 {
		if history.CommentID != commentID {
			ctx.NotFound("CompareCommentID", issues_model.ErrCommentNotExist{})
			return
		}

		if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
			log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
			return
		}
		if comment.IssueID != issue.ID {
			ctx.NotFound("CompareIssueID", issues_model.ErrCommentNotExist{})
			return
		}
	}

	canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
	if !canSoftDelete {
		ctx.JSON(http.StatusForbidden, map[string]any{
			"message": "Can not delete the content history",
		})
		return
	}

	err = issues_model.SoftDeleteIssueContentHistory(ctx, historyID)
	log.Debug("soft delete issue content history. issue=%d, comment=%d, history=%d", issue.ID, commentID, historyID)
	ctx.JSON(http.StatusOK, map[string]any{
		"ok": err == nil,
	})
}