package notifysubscriptions

import (
	"strings"

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

	"github.com/mattermost/mattermost-server/v6/shared/mlog"
)

func generateMarkdownDiff(oldText string, newText string, logger *mlog.Logger) string {
	oldTxtNorm := normalizeText(oldText)
	newTxtNorm := normalizeText(newText)

	dmp := diffmatchpatch.New()

	diffs := dmp.DiffMain(oldTxtNorm, newTxtNorm, false)

	diffs = dmp.DiffCleanupSemantic(diffs)
	diffs = dmp.DiffCleanupEfficiency(diffs)

	// check there is at least one insert or delete
	var editFound bool
	for _, d := range diffs {
		if (d.Type == diffmatchpatch.DiffInsert || d.Type == diffmatchpatch.DiffDelete) && strings.TrimSpace(d.Text) != "" {
			editFound = true
			break
		}
	}

	if !editFound {
		logger.Debug("skipping notification for superficial diff")
		return ""
	}

	cfg := markDownCfg{
		insertOpen:  "`",
		insertClose: "`",
		deleteOpen:  "~~`",
		deleteClose: "`~~",
	}
	markdown := generateMarkdown(diffs, cfg)
	markdown = strings.ReplaceAll(markdown, "¶", "\n")

	return markdown
}

const (
	truncLenEquals  = 60
	truncLenInserts = 120
	truncLenDeletes = 80
)

type markDownCfg struct {
	insertOpen  string
	insertClose string
	deleteOpen  string
	deleteClose string
}

func generateMarkdown(diffs []diffmatchpatch.Diff, cfg markDownCfg) string {
	sb := &strings.Builder{}

	var first, last bool

	for i, diff := range diffs {
		first = i == 0
		last = i == len(diffs)-1

		switch diff.Type {
		case diffmatchpatch.DiffInsert:
			sb.WriteString(cfg.insertOpen)
			sb.WriteString(truncate(diff.Text, truncLenInserts, first, last))
			sb.WriteString(cfg.insertClose)

		case diffmatchpatch.DiffDelete:
			sb.WriteString(cfg.deleteOpen)
			sb.WriteString(truncate(diff.Text, truncLenDeletes, first, last))
			sb.WriteString(cfg.deleteClose)

		case diffmatchpatch.DiffEqual:
			sb.WriteString(truncate(diff.Text, truncLenEquals, first, last))
		}
	}
	return sb.String()
}

func truncate(s string, maxLen int, first bool, last bool) string {
	if len(s) < maxLen {
		return s
	}

	var result string

	switch {
	case first:
		// truncate left
		result = " ... " + rightWords(s, maxLen)
	case last:
		// truncate right
		result = leftWords(s, maxLen) + " ... "
	default:
		// truncate in the middle
		half := len(s) / 2

		left := leftWords(s[:half], maxLen/2)
		right := rightWords(s[half:], maxLen/2)

		result = left + " ... " + right
	}

	return strings.ReplaceAll(result, "¶", "↩")
}

func normalizeText(s string) string {
	s = strings.ReplaceAll(s, "\t", " ")
	s = strings.ReplaceAll(s, "  ", " ")
	s = strings.ReplaceAll(s, "\n\n", "\n")
	s = strings.ReplaceAll(s, "\n", "¶")
	return s
}

// leftWords returns approximately maxLen characters from the left part of the source string by truncating on the right,
// with best effort to include whole words.
func leftWords(s string, maxLen int) string {
	if len(s) < maxLen {
		return s
	}
	fields := strings.Fields(s)
	fields = words(fields, maxLen)

	return strings.Join(fields, " ")
}

// rightWords returns approximately maxLen from the right part of the source string by truncating from the left,
// with best effort to include whole words.
func rightWords(s string, maxLen int) string {
	if len(s) < maxLen {
		return s
	}
	fields := strings.Fields(s)

	// reverse the fields so that the right-most words end up at the beginning.
	reverse(fields)

	fields = words(fields, maxLen)

	// reverse the fields again so that the original order is restored.
	reverse(fields)

	return strings.Join(fields, " ")
}

func reverse(ss []string) {
	ssLen := len(ss)
	for i := 0; i < ssLen/2; i++ {
		ss[i], ss[ssLen-i-1] = ss[ssLen-i-1], ss[i]
	}
}

// words returns a subslice containing approximately maxChars of characters. The last item may be truncated.
func words(words []string, maxChars int) []string {
	var count int
	result := make([]string, 0, len(words))

	for i, w := range words {
		wordLen := len(w)
		if wordLen+count > maxChars {
			switch {
			case i == 0:
				result = append(result, w[:maxChars])
			case wordLen < 8:
				result = append(result, w)
			}
			return result
		}
		count += wordLen
		result = append(result, w)
	}
	return result
}
