kudos.gno

package kudos

import (
	"chain"
	"chain/runtime/unsafe"
	"sort"
	"strconv"
	"strings"

	"gno.land/p/nt/avl/v0"
)

// counts maps an address (as string) -> number of kudos received.
var counts avl.Tree

// feed holds recent kudos, newest first, capped at feedCap entries.
var feed []kudo

const feedCap = 50

type kudo struct {
	from   address
	to     address
	reason string
}

// Give records a kudos from the caller to `to` with a reason.
// A caller cannot give kudos to themselves.
func Give(cur realm, to address, reason string) {
	from := unsafe.PreviousRealm().Address()
	if !to.IsValid() {
		panic("invalid recipient address")
	}
	if from == to {
		panic("cannot give kudos to yourself")
	}
	reason = strings.TrimSpace(reason)
	if reason == "" {
		panic("reason must not be empty")
	}

	key := to.String()
	n := int64(0)
	if v := counts.Get(key); v != nil {
		n = v.(int64)
	}
	counts.Set(key, n+1)

	// prepend newest, cap the feed
	feed = append([]kudo{{from: from, to: to, reason: reason}}, feed...)
	if len(feed) > feedCap {
		feed = feed[:feedCap]
	}

	chain.Emit("KudosGiven", "from", from.String(), "to", to.String(), "reason", reason)
}

// entry is a leaderboard row used for deterministic sorting.
type entry struct {
	addr  string
	count int64
}

// byScore sorts by count desc, then address asc for stable ties.
type byScore []entry

func (s byScore) Len() int      { return len(s) }
func (s byScore) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byScore) Less(i, j int) bool {
	if s[i].count != s[j].count {
		return s[i].count > s[j].count
	}
	return s[i].addr < s[j].addr
}

func leaderboard() []entry {
	out := make([]entry, 0, counts.Size())
	counts.Iterate("", "", func(key string, value any) bool {
		out = append(out, entry{addr: key, count: value.(int64)})
		return false
	})
	sort.Sort(byScore(out))
	return out
}

// Render shows a leaderboard of most-received addresses and a recent feed.
func Render(path string) string {
	var b strings.Builder
	b.WriteString("# Kudos\n\n")
	b.WriteString("Give props to addresses. `Give(to, reason)` records a kudos.\n\n")

	b.WriteString("## Leaderboard\n\n")
	lb := leaderboard()
	if len(lb) == 0 {
		b.WriteString("_No kudos yet._\n\n")
	} else {
		b.WriteString("| # | Address | Kudos |\n")
		b.WriteString("| --- | --- | --- |\n")
		for i, e := range lb {
			b.WriteString("| " + strconv.Itoa(i+1) + " | " + e.addr + " | " + strconv.FormatInt(e.count, 10) + " |\n")
		}
		b.WriteString("\n")
	}

	b.WriteString("## Recent kudos\n\n")
	if len(feed) == 0 {
		b.WriteString("_Nothing here yet._\n")
	} else {
		for _, k := range feed {
			b.WriteString("- " + k.from.String() + " → " + k.to.String() + ": " + k.reason + "\n")
		}
	}

	return b.String()
}