package users

import (
	"regexp"
	"std"
	"strconv"
	"strings"

	"gno.land/p/avl"
)

//----------------------------------------
// Types

type User struct {
	address std.Address
	name    string
	profile string
	number  int
	invites int
	inviter std.Address
}

func (u *User) Render() string {
	str := "## user " + u.name + "\n" +
		"\n" +
		" * address = " + string(u.address) + "\n" +
		" * " + strconv.Itoa(u.invites) + " invites\n"
	if u.inviter != "" {
		str = str + " * invited by " + string(u.inviter) + "\n"
	}
	str = str + "\n" +
		u.profile + "\n"
	return str
}

func (u User) Name() string         { return u.name }
func (u User) Profile() string      { return u.profile }
func (u User) Address() std.Address { return u.address }

//----------------------------------------
// State

var (
	admin      std.Address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"
	name2User  *avl.Tree                   // Name -> *User
	addr2User  *avl.Tree                   // std.Address -> *User
	invites    *avl.Tree                   // string(inviter+":"+invited) -> true
	counter    int                         // user id counter
	minFee     int64       = 200 * 1000000 // minimum gnot must be paid to register.
	maxFeeMult int64       = 10            // maximum multiples of minFee accepted.
)

//----------------------------------------
// Top-level functions

func Register(inviter std.Address, name string, profile string) {
	// assert CallTx call.
	std.AssertOriginCall()
	// assert invited or paid.
	caller := std.GetCallerAt(2)
	if caller != std.GetOrigCaller() {
		panic("should not happen") // because std.AssertOrigCall().
	}
	sentCoins := std.GetOrigSend()
	minCoin := std.Coin{"ugnot", minFee}
	if inviter == "" {
		// banker := std.GetBanker(std.BankerTypeOrigSend)
		if len(sentCoins) == 1 && sentCoins[0].IsGTE(minCoin) {
			if sentCoins[0].Amount > minFee*maxFeeMult {
				panic("payment must not be greater than " + strconv.Itoa(int(minFee*maxFeeMult)))
			} else {
				// ok
			}
		} else {
			panic("payment must not be less than " + strconv.Itoa(int(minFee)))
		}
	} else {
		invitekey := inviter.String() + ":" + caller.String()
		_, _, ok := invites.Get(invitekey)
		if !ok {
			panic("invalid invitation")
		}
		invites.Remove(invitekey)
	}
	// assert not already registered.
	_, _, ok := name2User.Get(name)
	if ok {
		panic("name already registered")
	}
	_, _, ok = addr2User.Get(caller.String())
	if ok {
		panic("address already registered")
	}
	// assert name is valid.
	if !reName.MatchString(name) {
		panic("invalid name: " + name + " (must be at least 6 characters, lowercase alphanumeric with underscore)")
	}
	// remainder of fees go toward invites.
	invites := int(0)
	if len(sentCoins) == 1 {
		if sentCoins[0].Denom == "ugnot" && sentCoins[0].Amount >= minFee {
			invites = int(sentCoins[0].Amount / minFee)
			if inviter == "" && invites > 0 {
				invites -= 1
			}
		}
	}
	// register.
	counter++
	user := &User{
		address: caller,
		name:    name,
		profile: profile,
		number:  counter,
		invites: invites,
		inviter: inviter,
	}
	name2User, _ = name2User.Set(name, user)
	addr2User, _ = addr2User.Set(caller.String(), user)
}

func Invite(invitee string) {
	// assert CallTx call.
	std.AssertOriginCall()
	// get caller/inviter.
	caller := std.GetCallerAt(2)
	if caller != std.GetOrigCaller() {
		panic("should not happen") // because std.AssertOrigCall().
	}
	lines := strings.Split(invitee, "\n")
	if caller == admin {
		// nothing to do, all good
	} else {
		// ensure has invites.
		_, userI, ok := addr2User.Get(caller.String())
		if !ok {
			panic("user unknown")
		}
		user := userI.(*User)
		if user.invites <= 0 {
			panic("user has no invite tokens")
		}
		user.invites -= len(lines)
		if user.invites < 0 {
			panic("user has insufficient invite tokens")
		}
	}
	// for each line...
	for _, line := range lines {
		if line == "" {
			continue // file bodies have a trailing newline.
		} else if strings.HasPrefix(line, `//`) {
			continue // comment
		}
		// record invite.
		invitekey := string(caller) + ":" + string(line)
		invites, _ = invites.Set(invitekey, true)
	}
}

func GrantInvites(invites string) {
	// assert CallTx call.
	std.AssertOriginCall()
	// assert admin.
	caller := std.GetCallerAt(2)
	if caller != std.GetOrigCaller() {
		panic("should not happen") // because std.AssertOrigCall().
	}
	if caller != admin {
		panic("unauthorized")
	}
	// for each line...
	lines := strings.Split(invites, "\n")
	for _, line := range lines {
		if line == "" {
			continue // file bodies have a trailing newline.
		} else if strings.HasPrefix(line, `//`) {
			continue // comment
		}
		// parse name and invites.
		var name string
		var invites int
		parts := strings.Split(line, ":")
		if len(parts) == 1 { // short for :1.
			name = parts[0]
			invites = 1
		} else if len(parts) == 2 {
			name = parts[0]
			invites_, err := strconv.Atoi(parts[1])
			if err != nil {
				panic(err)
			}
			invites = int(invites_)
		} else {
			panic("should not happen")
		}
		// give invites.
		_, userI, ok := name2User.Get(name)
		if !ok {
			// maybe address.
			_, userI, ok = addr2User.Get(name)
			if !ok {
				panic("invalid user " + name)
			}
		}
		user := userI.(*User)
		user.invites += invites
	}
}

// Any leftover fees go toward invitations.
func SetMinFee(newMinFee int64) {
	// assert CallTx call.
	std.AssertOriginCall()
	// assert admin caller.
	caller := std.GetCallerAt(2)
	if caller != admin {
		panic("unauthorized")
	}
	// update global variables.
	minFee = newMinFee
}

// This helps prevent fat finger accidents.
func SetMaxFeeMultiple(newMaxFeeMult int64) {
	// assert CallTx call.
	std.AssertOriginCall()
	// assert admin caller.
	caller := std.GetCallerAt(2)
	if caller != admin {
		panic("unauthorized")
	}
	// update global variables.
	maxFeeMult = newMaxFeeMult
}

//----------------------------------------
// Exposed public functions

func GetUserByName(name string) *User {
	_, userI, ok := name2User.Get(name)
	if !ok {
		return nil
	}
	return userI.(*User)
}

func GetUserByAddress(addr std.Address) *User {
	_, userI, ok := addr2User.Get(addr.String())
	if !ok {
		return nil
	}
	return userI.(*User)
}

// unlike GetUserByName, input must be "@" prefixed for names.
func GetUserByAddressOrName(input AddressOrName) *User {
	name, isName := input.GetName()
	if isName {
		return GetUserByName(name)
	}
	return GetUserByAddress(std.Address(input))
}

//----------------------------------------
// Constants

// NOTE: name length must be clearly distinguishable from a bech32 address.
var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`)

//----------------------------------------
// Render main page

func Render(path string) string {
	if path == "" {
		return renderHome()
	} else if len(path) >= 38 { // 39? 40?
		if path[:2] != "g1" {
			return "invalid address " + path
		}
		user := GetUserByAddress(std.Address(path))
		if user == nil {
			// TODO: display basic information about account.
			return "unknown address " + path
		}
		return user.Render()
	} else {
		user := GetUserByName(path)
		if user == nil {
			return "unknown username " + path
		}
		return user.Render()
	}
}

func renderHome() string {
	doc := ""
	name2User.Iterate("", "", func(t *avl.Tree) bool {
		user := t.Value().(*User)
		doc += " * [" + user.name + "](/r/users:" + user.name + ")\n"
		return false
	})
	return doc
}