package plugins

import (
	"fmt"
	"github.com/alexandre-normand/slackscot"
	"github.com/alexandre-normand/slackscot/actions"
	"github.com/alexandre-normand/slackscot/plugin"
	"github.com/alexandre-normand/slackscot/store"
	"github.com/slack-go/slack"
	"log"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

// Karma holds the plugin data for the karma plugin
type Karma struct {
	*slackscot.Plugin
	karmaStorer store.GlobalSiloStringStorer
}

const (
	// KarmaPluginName holds identifying name for the karma plugin
	KarmaPluginName  = "karma"
	defaultItemCount = 5
)

var karmaRegex = regexp.MustCompile("(?:(<(@[\\w']+)>\\s?))(\\+{2,6}|\\-{2,6})")

// Ranker represents attributes and behavior to process a ranking list
type ranker struct {
	name             string
	regexp           *regexp.Regexp
	bannerText       string
	bannerImgLink    string
	bannerImgAltText string
	scanner          karmaScanner
	sorter           karmaSorter
}

var globalTopRanker ranker
var topRanker ranker
var globalWorstRanker ranker
var worstRanker ranker

func init() {
	globalTopRanker = ranker{name: "global top",
		regexp:     regexp.MustCompile("(?i)\\A(global top)+(?:\\s+(\\d*))*\\z"),
		bannerText: ":leaves::leaves::leaves::trophy: *Global Top* :trophy::leaves::leaves::leaves:",
		scanner:    scanGlobalKarma,
		sorter:     sortTop}

	topRanker = ranker{name: "top",
		regexp:     regexp.MustCompile("(?i)\\A(top)+(?:\\s+(\\d*))*\\z"),
		bannerText: ":leaves::leaves::leaves::trophy: *Top* :trophy::leaves::leaves::leaves:",
		scanner:    scanChannelKarma,
		sorter:     sortTop}

	globalWorstRanker = ranker{name: "global worst",
		regexp:     regexp.MustCompile("(?i)\\A(global worst)+(?:\\s+(\\d*))*\\z"),
		bannerText: ":fallen_leaf::fallen_leaf::fallen_leaf::space_invader: *Global Worst* :space_invader::fallen_leaf::fallen_leaf::fallen_leaf:",
		scanner:    scanGlobalKarma,
		sorter:     sortWorst}

	worstRanker = ranker{name: "worst",
		regexp:     regexp.MustCompile("(?i)\\A(worst)+(?:\\s+(\\d*))*\\z"),
		bannerText: ":fallen_leaf::fallen_leaf::fallen_leaf::space_invader: *Worst* :space_invader::fallen_leaf::fallen_leaf::fallen_leaf:",
		scanner:    scanChannelKarma,
		sorter:     sortWorst}
}

// NewKarma creates a new instance of the Karma plugin
func NewKarma(storer store.GlobalSiloStringStorer) (karma *slackscot.Plugin) {
	k := new(Karma)

	k.Plugin = plugin.New(KarmaPluginName).
		WithCommandNamespacing().
		WithCommand(actions.NewCommand().
			WithMatcher(matchKarmaTopReport).
			WithUsage("top [count]").
			WithDescriptionf("Return the top things ever recorded in this channel (default of %d items)", defaultItemCount).
			WithAnswerer(k.answerKarmaTop).
			Build()).
		WithCommand(actions.NewCommand().
			WithMatcher(matchKarmaWorstReport).
			WithUsage("worst [count]").
			WithDescriptionf("Return the worst things ever recorded in this channel (default of %d items)", defaultItemCount).
			WithAnswerer(k.answerKarmaWorst).
			Build()).
		WithCommand(actions.NewCommand().
			WithMatcher(matchGlobalKarmaTopReport).
			WithUsage("global top [count]").
			WithDescriptionf("Return the top things ever over all channels (default of %d items)", defaultItemCount).
			WithAnswerer(k.answerGlobalKarmaTop).
			Build()).
		WithCommand(actions.NewCommand().
			WithMatcher(matchGlobalKarmaWorstReport).
			WithUsage("global worst [count]").
			WithDescriptionf("Return the worst things ever over all channels (default of %d items)", defaultItemCount).
			WithAnswerer(k.answerGlobalKarmaWorst).
			Build()).
		WithCommand(actions.NewCommand().
			Hidden().
			WithMatcher(matchKarmaReset).
			WithUsage("reset").
			WithDescription("Resets all recorded karma for the current channel").
			WithAnswerer(k.clearChannelKarma).
			Build()).
		WithHearAction(actions.NewCommand().
			WithMatcher(matchKarmaRecord).
			WithUsage("thing++ or thing--").
			WithDescription("Keep track of karma. Increments larger than `1` (up to `5`) can be achieved with extra `+` or `-` signs").
			WithAnswerer(k.recordKarma).
			Build()).
		Build()

	k.karmaStorer = storer

	return k.Plugin
}

// matchKarmaRecord returns true if the message matches karma++ or karma-- (karma being any word)
func matchKarmaRecord(m *slackscot.IncomingMessage) bool {
	matches := karmaRegex.FindStringSubmatch(m.NormalizedText)
	return len(matches) > 0
}

// matchKarmaTopReport returns true if the message matches a request for top karma with
// a message such as "top <count>"
func matchKarmaTopReport(m *slackscot.IncomingMessage) bool {
	return topRanker.regexp.MatchString(m.NormalizedText)
}

// matchKarmaWorstReport returns true if the message matches a request for the worst karma with
// a message such as "worst <count>"
func matchKarmaWorstReport(m *slackscot.IncomingMessage) bool {
	return worstRanker.regexp.MatchString(m.NormalizedText)
}

// matchGlobalKarmaTopReport returns true if the message matches a request for top global karma with
// a message such as "global top <count>"
func matchGlobalKarmaTopReport(m *slackscot.IncomingMessage) bool {
	return globalTopRanker.regexp.MatchString(m.NormalizedText)
}

// matchGlobalKarmaWorstReport returns true if the message matches a request for the worst global karma with
// a message such as "global worst <count>"
func matchGlobalKarmaWorstReport(m *slackscot.IncomingMessage) bool {
	return globalWorstRanker.regexp.MatchString(m.NormalizedText)
}

// matchKarmaReset returns true if the message matches a request for resetting karma with a
// message such as "reset"
func matchKarmaReset(m *slackscot.IncomingMessage) bool {
	return strings.HasPrefix(m.NormalizedText, "reset")
}

// recordKarma records a karma increase or decrease and answers with a message including
// the recorded word with its associated karma value
func (k *Karma) recordKarma(message *slackscot.IncomingMessage) *slackscot.Answer {
	matches := karmaRegex.FindAllStringSubmatch(message.Text, -1)
	answerText := ""
	
	for idx, match := range matches {
		
		// only add newlines if more than one match
		if idx > 0 {
			answerText += "\n"
		}
		
		thing := match[2]
		// Prevent a user from attributing karma to self
		if strings.TrimPrefix(thing, "@") == message.User {
			return &slackscot.Answer{Text: "*Attributing yourself karma is frown upon* :face_with_raised_eyebrow:", Options: []slackscot.AnswerOption{slackscot.AnswerEphemeral(message.User)}}
		}

		rawValue, err := k.karmaStorer.GetSiloString(message.Channel, thing)
		if err != nil {
			rawValue = "0"
		}
		karma, err := strconv.Atoi(rawValue)
		if err != nil {
			k.Logger.Printf("[%s] Error parsing current karma value [%s], something's wrong and resetting to 0: %v", KarmaPluginName, rawValue, err)
			karma = 0
		}

		log.Printf("thing is [%s]\n", thing)
		renderedThing := k.renderThing(thing)

		instruction := match[3]
		if strings.HasPrefix(instruction, "+") {
			incrementSymbols := strings.TrimPrefix(instruction, "+")
			increment := len(incrementSymbols)
			karma = karma + increment

			if increment == 1 {
				answerText += fmt.Sprintf("`%s` just gained karma (`%s`: %d)", renderedThing, renderedThing, karma)
			} else {
				answerText += fmt.Sprintf("`%s` just gained %d karma points (`%s`: %d)", renderedThing, increment, renderedThing, karma)
			}

		} else {
			decrementSymbols := strings.TrimPrefix(instruction, "-")
			decrement := len(decrementSymbols)
			karma = karma - decrement

			if decrement == 1 {
				answerText += fmt.Sprintf("`%s` just lost karma (`%s`: %d)", renderedThing, renderedThing, karma)
			} else {
				answerText += fmt.Sprintf("`%s` just lost %d karma points (`%s`: %d)", renderedThing, decrement, renderedThing, karma)
			}
		}

		// Store new value
		err = k.karmaStorer.PutSiloString(message.Channel, thing, strconv.Itoa(karma))
		if err != nil {
			k.Logger.Printf("[%s] Error persisting karma: %v", KarmaPluginName, err)
			return nil
		}
	}



	return &slackscot.Answer{Text: answerText}
}

// renderThing renders the thing value. In most cases, it should just return the value
// untouched but if it starts with '@', it tries to find the user info matching the value
// and returns that instead (if found a match)
func (k *Karma) renderThing(thing string) (renderedThing string) {
	if strings.HasPrefix(thing, "@") {
		u, _ := k.UserInfoFinder.GetUserInfo(strings.TrimPrefix(thing, "@"))

		if u != nil {
			return u.RealName
		}
	}

	return thing
}

// answerKarmaTop returns an answer with the top list of karma entries for the channel the message is received on
func (k *Karma) answerKarmaTop(m *slackscot.IncomingMessage) *slackscot.Answer {
	return k.answerKarmaRankList(m, topRanker)
}

// answerKarmaTop returns an answer with the list of worst karma entries for the channel the message is received on
func (k *Karma) answerKarmaWorst(m *slackscot.IncomingMessage) *slackscot.Answer {
	return k.answerKarmaRankList(m, worstRanker)
}

// answerKarmaTop returns an answer with the top list of karma entries for all channels
func (k *Karma) answerGlobalKarmaTop(m *slackscot.IncomingMessage) *slackscot.Answer {
	return k.answerKarmaRankList(m, globalTopRanker)
}

// answerKarmaTop returns an answer with the list of worst karma entries for all channels
func (k *Karma) answerGlobalKarmaWorst(m *slackscot.IncomingMessage) *slackscot.Answer {
	return k.answerKarmaRankList(m, globalWorstRanker)
}

// clearChannelKarma processes a request to clear karma in a channel (the message's channel is used to tell which one)
func (k *Karma) clearChannelKarma(m *slackscot.IncomingMessage) *slackscot.Answer {
	entries, err := k.karmaStorer.ScanSilo(m.Channel)
	if err != nil {
		return &slackscot.Answer{Text: fmt.Sprintf("Sorry, I couldn't get delete karma for channel [%s] for you. If you must know, this happened: %s", m.Channel, err.Error())}
	}

	for thing := range entries {
		err = k.karmaStorer.DeleteSiloString(m.Channel, thing)
	}

	if err != nil {
		return &slackscot.Answer{Text: fmt.Sprintf("Sorry, I couldn't get delete karma for channel [%s] for you. If you must know, this happened: %s", m.Channel, err.Error())}
	}

	return &slackscot.Answer{Text: "karma all cleared :white_check_mark::boom:"}
}

// karmaSorter is a function sorting pairList of karma entries. Used to plug in top/worst sorting
type karmaSorter func(pl pairList)

// sortWorst sorts karma from the lowest value to the highest
func sortWorst(pl pairList) {
	sort.Sort(pl)
}

// sortWorst sorts karma from the highest to lowest
func sortTop(pl pairList) {
	sort.Sort(sort.Reverse(pl))
}

// karmaScanner is a function that returns karma entries for a given channel. It is used
// to plug in different behaviors like channel scanning and global scanning
type karmaScanner func(karmaStorer store.GlobalSiloStringStorer, channelID string) (entries map[string]string, err error)

// scanChannelKarma scans the silo for the given channel id and returns only the entries for that
// channel
func scanChannelKarma(karmaStorer store.GlobalSiloStringStorer, channelID string) (entries map[string]string, err error) {
	return karmaStorer.ScanSilo(channelID)
}

// scanGlobalKarma invokes a GlobalScan and merges karma over all channels. If there's
// an error, a nil map is returned along with that error
func scanGlobalKarma(karmaStorer store.GlobalSiloStringStorer, channelID string) (entries map[string]string, err error) {
	entriesByChannel, err := karmaStorer.GlobalScan()
	if err != nil {
		return nil, err
	}

	entries = make(map[string]string)
	for _, chEntries := range entriesByChannel {
		for thing, val := range chEntries {
			if _, ok := entries[thing]; !ok {
				entries[thing] = val
			} else {
				entries[thing], err = mergeKarma(entries[thing], val)
				if err != nil {
					return nil, err
				}
			}
		}
	}

	return entries, nil
}

// mergeKarma merges two values assumed to be strings holding integers and
// returns the sum as a string
func mergeKarma(v1 string, v2 string) (merged string, err error) {
	val1, err := strconv.Atoi(v1)
	if err != nil {
		return "", err
	}

	val2, err := strconv.Atoi(v2)
	if err != nil {
		return "", err
	}

	return strconv.Itoa(val1 + val2), nil
}

// answerKarmaRankList returns an answer for a ranked list request according to the behavior and attributes of the given ranker
func (k *Karma) answerKarmaRankList(m *slackscot.IncomingMessage, ranker ranker) *slackscot.Answer {
	match := ranker.regexp.FindAllStringSubmatch(m.NormalizedText, -1)[0]

	count := defaultItemCount
	rawCount := match[2]
	if len(rawCount) > 0 {
		count, _ = strconv.Atoi(rawCount)
	}

	values, err := ranker.scanner(k.karmaStorer, m.Channel)
	if err != nil {
		return &slackscot.Answer{Text: fmt.Sprintf("Sorry, I couldn't get the %s [%d] things for you. If you must know, this happened: %v", ranker.name, count, err)}
	}

	pairs, err := getRankedList(values, count, ranker.sorter)
	if err != nil {
		return &slackscot.Answer{Text: fmt.Sprintf("Sorry, I couldn't get the %s [%d] things for you. If you must know, this happened: %v", ranker.name, count, err)}
	}

	if len(pairs) > 0 {
		blocks := make([]slack.Block, 0)

		blocks = append(blocks, slack.NewSectionBlock(slack.NewTextBlockObject("mrkdwn", ranker.bannerText, false, false), nil, nil))
		blocks = append(blocks, k.formatList(pairs)...)

		return &slackscot.Answer{Text: "", ContentBlocks: blocks}
	}

	return &slackscot.Answer{Text: "Sorry, no recorded karma found :disappointed:"}
}

// formatList formats a list of ranked items using the rankRenderer to render the rank icons and returns the resulting block kit blocks
func (k *Karma) formatList(pl pairList) (blocks []slack.Block) {
	blocks = make([]slack.Block, 0)

	rank := 1
	for _, pair := range pl {
		blocks = append(blocks, formatRankedElement(pair, rank))
		rank = rank + 1
	}

	return blocks
}

// formatRankedElement formats one ranked element in a list. It adds 3 blocks: one for the rank (icon),
// one for the ranked "thing" and one for its karma value. The 3 block objects are then wrapped in a context block
func formatRankedElement(p pair, rank int) (block slack.Block) {
	return *slack.NewSectionBlock(slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("• %s `%d`", renderThingName(p.Key), p.Value), false, false), nil, nil)
}

// renderThingName renders a karma item by formatting a user id with the required symbols such that it looks
// like <@userId>. For things that aren't user ids, the value is returned as-is
func renderThingName(thing string) (render string) {
	if strings.HasPrefix(thing, "@") {
		return "<" + thing + ">"
	}

	return thing
}

// pair holds a key (thing name) and its count
type pair struct {
	Key   string
	Value int
}

// pairList adapted from Andrew Gerrand for a similar problem: https://groups.google.com/forum/#!topic/golang-nuts/FT7cjmcL7gw
type pairList []pair

func (p pairList) Len() int { return len(p) }

func (p pairList) Less(i, j int) bool {
	return p[i].Value < p[j].Value || (p[i].Value == p[j].Value && strings.Compare(p[i].Key, p[j].Key) > 0)
}

func (p pairList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

func convertToPairs(wordFrequencies map[string]int) pairList {
	pl := make(pairList, len(wordFrequencies))
	i := 0
	for k, v := range wordFrequencies {
		pl[i] = pair{k, v}
		i++
	}

	return pl
}

func getRankedList(rawData map[string]string, count int, sort karmaSorter) (results pairList, err error) {
	wordWithFrequencies, err := convertMapValues(rawData)
	if err != nil {
		return results, err
	}

	pl := convertToPairs(wordWithFrequencies)

	sort(pl)

	limit := count

	if len(pl) < count {
		limit = len(pl)
	}
	return pl[:limit], nil
}

func convertMapValues(rawData map[string]string) (result map[string]int, err error) {
	result = map[string]int{}

	for k, v := range rawData {
		result[k], err = strconv.Atoi(v)
		if err != nil {
			return result, err
		}
	}

	return result, nil
}
