staker_delegate.gno

package staker

import (
	"chain"
	"chain/runtime"
	"errors"
	"time"

	gnsmath "gno.land/p/gnoswap/gnsmath"
	"gno.land/p/gnoswap/utils"
	"gno.land/r/gnoswap/access"
	"gno.land/r/gnoswap/emission"
	"gno.land/r/gnoswap/gns"

	"gno.land/r/gnoswap/gov/staker"
	"gno.land/r/gnoswap/gov/xgns"
	"gno.land/r/gnoswap/halt"
	"gno.land/r/gnoswap/referral"
)

// Spoofed-realm guards on entry points reject any caller that fabricates a
// realm value distinct from the current crossing frame. The proxy in
// gno.land/r/gnoswap/gov/staker always forwards its own `cur`, so a mismatch
// here means somebody bypassed the proxy and threaded a fake realm directly
// into the implementation.

// Delegate delegates GNS tokens to an address.
//
// Converts GNS to xGNS and assigns voting power.
// Primary mechanism for participating in governance.
// Can delegate to self or any other address.
//
// Parameters:
//   - to: Address to receive voting power (can be self)
//   - amount: Amount of GNS to stake and delegate
//   - referrer: Optional referral address for tracking
//
// Process:
//  1. Transfers GNS from caller
//  2. Mints equivalent xGNS (1:1 ratio)
//  3. Assigns voting power to target address
//  4. Creates delegation snapshot for voting
//
// Requirements:
//   - Minimum 1 GNS delegation
//   - Valid target address
//   - Sufficient GNS balance
//   - Approval for GNS transfer
//
// Returns delegated amount.
func (gs *govStakerV1) Delegate(
	_ int,
	rlm realm,
	to address,
	amount int64,
	referrer string,
) int64 {
	if !rlm.IsCurrent() {
		panic(errors.New(errSpoofedRealm))
	}

	halt.AssertIsNotHaltedGovStaker()

	prev := rlm.Previous()
	access.AssertIsValidAddress(to)

	assertIsValidDelegateAmount(amount)

	caller := prev.Address()
	from := caller
	currentHeight := runtime.ChainHeight()
	currentTimestamp := time.Now().Unix()

	emission.MintAndDistributeGns(cross(rlm))

	delegation, err := gs.delegate(
		0,
		rlm,
		from,
		to,
		amount,
		currentHeight,
		currentTimestamp,
	)
	if err != nil {
		panic(err)
	}

	if err := gs.increaseTotalDelegatedAmount(0, rlm, amount); err != nil {
		panic(err)
	}
	if err := gs.increaseTotalLockedAmount(0, rlm, amount); err != nil {
		panic(err)
	}

	gns.TransferFrom(cross(rlm), from, rlm.Address(), amount)
	xgns.Mint(cross(rlm), from, amount)

	registeredReferrer := referral.TryRegister(cross(rlm), caller, referrer)

	resolver := NewDelegationResolver(delegation)

	chain.Emit(
		"Delegate",
		"prevAddr", prev.Address().String(),
		"prevRealm", prev.PkgPath(),
		"from", resolver.delegation.DelegateFrom().String(),
		"to", resolver.delegation.DelegateTo().String(),
		"amount", utils.FormatInt(resolver.DelegatedAmount()),
		"totalDelegatedAmount", utils.FormatInt(gs.store.GetTotalDelegatedAmount()),
		"referrer", registeredReferrer,
	)

	return amount
}

// Undelegate undelegates xGNS from the existing delegate.
//
// Initiates withdrawal of staked GNS with lockup period.
// Voting power removed immediately, tokens locked for configurable period.
// Prevents governance attacks through time delay.
//
// Parameters:
//   - from: Address currently delegated to
//   - amount: Amount of xGNS to undelegate
//
// Process:
//  1. Removes voting power immediately
//  2. Creates withdrawal request with timestamp
//  3. Locks GNS for configurable cooldown period
//
// Requirements:
//   - Must have delegated to target address
//   - Sufficient delegated amount
//
// After lockup period ends, use CollectUndelegatedGns() to claim GNS.
// Returns undelegated amount.
func (gs *govStakerV1) Undelegate(
	_ int,
	rlm realm,
	from address,
	amount int64,
) int64 {
	if !rlm.IsCurrent() {
		panic(errors.New(errSpoofedRealm))
	}

	halt.AssertIsNotHaltedWithdraw()

	prev := rlm.Previous()
	caller := prev.Address()
	access.AssertIsValidAddress(from)

	assertIsValidDelegateAmount(amount)

	currentHeight := runtime.ChainHeight()
	currentTimestamp := time.Now().Unix()

	emission.MintAndDistributeGns(cross(rlm))

	unDelegationAmount, err := gs.unDelegate(
		0,
		rlm,
		caller,
		from,
		amount,
		currentHeight,
		currentTimestamp,
	)
	if err != nil {
		panic(err)
	}

	if err := gs.decreaseTotalDelegatedAmount(0, rlm, unDelegationAmount); err != nil {
		panic(err)
	}

	chain.Emit(
		"Undelegate",
		"prevAddr", prev.Address().String(),
		"prevRealm", prev.PkgPath(),
		"from", caller.String(),
		"to", from.String(),
		"amount", utils.FormatInt(unDelegationAmount),
		"totalDelegatedAmount", utils.FormatInt(gs.store.GetTotalDelegatedAmount()),
	)

	return unDelegationAmount
}

// Redelegate redelegates xGNS from existing delegate to another.
//
// Atomic operation to change delegation target.
// Maintains voting power continuity without unstaking.
// Useful for vote delegation services and dao coordination.
//
// Parameters:
//   - delegatee: Current address delegated to
//   - newDelegatee: New address to delegate to
//   - amount: Amount of xGNS to redelegate
//
// Process:
//  1. Validates current delegation exists
//  2. Removes voting power from old delegatee
//  3. Assigns voting power to new delegatee
//  4. Updates delegation snapshots
//
// Requirements:
//   - Must have active delegation to current delegatee
//   - Both addresses must be valid
//   - Amount must not exceed current delegation
//   - Cannot redelegate to same address
//
// No lockup period - instant redelegation.
// Returns redelegated amount.
func (gs *govStakerV1) Redelegate(
	_ int,
	rlm realm,
	delegatee,
	newDelegatee address,
	amount int64,
) int64 {
	if !rlm.IsCurrent() {
		panic(errors.New(errSpoofedRealm))
	}

	halt.AssertIsNotHaltedGovStaker()

	prev := rlm.Previous()
	caller := prev.Address()
	access.AssertIsValidAddress(delegatee)
	access.AssertIsValidAddress(newDelegatee)

	assertIsValidDelegateAmount(amount)
	assertNoSameDelegatee(delegatee, newDelegatee)

	currentHeight := runtime.ChainHeight()
	currentTimestamp := time.Now().Unix()
	delegator := caller

	emission.MintAndDistributeGns(cross(rlm))

	unDelegationAmount, err := gs.unDelegateWithoutLockup(
		0,
		rlm,
		delegator,
		delegatee,
		amount,
		currentHeight,
		currentTimestamp,
	)
	if err != nil {
		panic(err)
	}

	delegation, err := gs.delegate(
		0,
		rlm,
		delegator,
		newDelegatee,
		unDelegationAmount,
		currentHeight,
		currentTimestamp,
	)
	if err != nil {
		panic(err)
	}

	resolver := NewDelegationResolver(delegation)
	chain.Emit(
		"Redelegate",
		"prevAddr", prev.Address().String(),
		"prevRealm", prev.PkgPath(),
		"from", delegator.String(),
		"previousDelegatee", delegatee.String(),
		"newDelegatee", newDelegatee.String(),
		"amount", utils.FormatInt(resolver.DelegatedAmount()),
	)

	return amount
}

// CollectUndelegatedGns collects undelegated GNS tokens.
// Allows users to collect GNS tokens that completed undelegation lockup period.
// Burns xGNS and returns GNS tokens.
func (gs *govStakerV1) CollectUndelegatedGns(_ int, rlm realm) int64 {
	if !rlm.IsCurrent() {
		panic(errors.New(errSpoofedRealm))
	}

	halt.AssertIsNotHaltedWithdraw()

	prev := rlm.Previous()
	caller := prev.Address()
	currentTime := time.Now().Unix()

	emission.MintAndDistributeGns(cross(rlm))

	collectedAmount, err := gs.collectDelegations(0, rlm, caller, currentTime)
	if err != nil {
		panic(err)
	}

	if collectedAmount == 0 {
		return 0
	}

	if err := gs.decreaseTotalLockedAmount(0, rlm, collectedAmount); err != nil {
		panic(err)
	}

	xgns.Burn(cross(rlm), caller, collectedAmount)
	gns.Transfer(cross(rlm), caller, collectedAmount)

	chain.Emit(
		"CollectUndelegatedGns",
		"prevAddr", prev.Address().String(),
		"prevRealm", prev.PkgPath(),
		"from", prev.Address().String(),
		"to", caller.String(),
		"collectedAmount", utils.FormatInt(collectedAmount),
	)

	return collectedAmount
}

// delegate processes delegation operations.
// Validates delegation amount, creates delegation records, and updates reward tracking.
func (gs *govStakerV1) delegate(
	_ int,
	rlm realm,
	from address,
	to address,
	amount,
	currentHeight,
	currentTimestamp int64,
) (*staker.Delegation, error) {
	delegationID := gs.nextDelegationID()
	delegation := staker.NewDelegation(
		delegationID,
		from,
		to,
		amount,
		currentHeight,
		currentTimestamp,
	)
	delegationResolver := NewDelegationResolver(delegation)
	delegatedAmount := delegationResolver.DelegatedAmount()
	if delegatedAmount < 0 {
		return nil, errors.New("delegated amount cannot be negative")
	}

	gs.addDelegation(0, rlm, delegationID, delegation)
	gs.addDelegationRecord(0, rlm, to, delegatedAmount, currentTimestamp)
	gs.addStakeEmissionReward(0, rlm, from.String(), amount, currentTimestamp)
	gs.addStakeProtocolFeeReward(0, rlm, from.String(), amount, currentTimestamp)

	return delegation, nil
}

// unDelegate processes undelegation operations with lockup.
// Validates undelegation amount, processes withdrawals, and updates reward tracking.
func (gs *govStakerV1) unDelegate(
	_ int,
	rlm realm,
	delegator,
	delegatee address,
	amount,
	currentHeight,
	currentTimestamp int64,
) (int64, error) {
	delegationIDs := gs.getUserDelegationIDsWithDelegatee(delegator, delegatee)
	if len(delegationIDs) == 0 {
		return 0, nil
	}

	unDelegationAmount := amount
	lockupPeriod := gs.store.GetUnDelegationLockupPeriod()
	totalDelegated := int64(0)
	delegations := make([]*staker.Delegation, 0, len(delegationIDs))

	for _, id := range delegationIDs {
		delegation, exists := gs.store.GetDelegation(id)
		if !exists {
			continue
		}

		totalDelegated = gnsmath.SafeAddInt64(totalDelegated, NewDelegationResolver(delegation).DelegatedAmount())
		delegations = append(delegations, delegation)
	}

	if amount > totalDelegated {
		return 0, errors.New(errNotEnoughDelegated)
	}

	// Process undelegation across multiple delegation records if necessary
	for _, delegation := range delegations {
		resolver := NewDelegationResolver(delegation)
		if resolver.IsEmpty() {
			gs.removeDelegation(0, rlm, delegation.ID())
			continue
		}

		currentUnDelegationAmount := unDelegationAmount

		if currentUnDelegationAmount > resolver.DelegatedAmount() {
			currentUnDelegationAmount = resolver.DelegatedAmount()
		}

		if currentUnDelegationAmount < 0 {
			return 0, errors.New("undelegation amount cannot be negative")
		}

		resolver.UnDelegate(
			currentUnDelegationAmount,
			currentHeight,
			currentTimestamp,
			lockupPeriod,
		)

		gs.setDelegation(0, rlm, delegation.ID(), delegation)
		gs.addDelegationRecord(0, rlm, delegatee, -currentUnDelegationAmount, currentTimestamp)
		gs.removeStakeEmissionReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTimestamp)
		gs.removeStakeProtocolFeeReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTimestamp)

		unDelegationAmount = gnsmath.SafeSubInt64(unDelegationAmount, currentUnDelegationAmount)
		if unDelegationAmount <= 0 {
			break
		}
	}

	return amount, nil
}

// unDelegateWithoutLockup processes undelegation without lockup.
// Used for redelegation where tokens are immediately available.
func (gs *govStakerV1) unDelegateWithoutLockup(
	_ int,
	rlm realm,
	delegator,
	delegatee address,
	amount,
	currentHeight,
	currentTime int64,
) (int64, error) {
	delegationIDs := gs.getUserDelegationIDsWithDelegatee(delegator, delegatee)
	if len(delegationIDs) == 0 {
		return 0, errors.New(errNotEnoughDelegated)
	}

	unDelegationAmount := amount
	totalDelegated := int64(0)
	delegations := make([]*staker.Delegation, 0, len(delegationIDs))

	for _, id := range delegationIDs {
		delegation, exists := gs.store.GetDelegation(id)
		if !exists {
			continue
		}

		totalDelegated = gnsmath.SafeAddInt64(totalDelegated, NewDelegationResolver(delegation).DelegatedAmount())
		delegations = append(delegations, delegation)
	}

	if amount > totalDelegated {
		return 0, errors.New(errNotEnoughDelegated)
	}

	// Process undelegation across multiple delegation records if necessary
	for _, delegation := range delegations {
		resolver := NewDelegationResolver(delegation)
		if resolver.IsEmpty() {
			gs.removeDelegation(0, rlm, delegation.ID())
			continue
		}

		currentUnDelegationAmount := unDelegationAmount

		if currentUnDelegationAmount > resolver.DelegatedAmount() {
			currentUnDelegationAmount = resolver.DelegatedAmount()
		}

		resolver.UnDelegateWithoutLockup(
			currentUnDelegationAmount,
			currentHeight,
			currentTime,
		)

		if resolver.IsEmpty() {
			gs.removeDelegation(0, rlm, delegation.ID())
		} else {
			gs.setDelegation(0, rlm, delegation.ID(), delegation)
		}
		gs.addDelegationRecord(0, rlm, delegatee, -currentUnDelegationAmount, currentTime)
		gs.removeStakeEmissionReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTime)
		gs.removeStakeProtocolFeeReward(0, rlm, delegator.String(), currentUnDelegationAmount, currentTime)

		unDelegationAmount = gnsmath.SafeSubInt64(unDelegationAmount, currentUnDelegationAmount)
		if unDelegationAmount <= 0 {
			break
		}
	}

	return amount, nil
}

func (gs *govStakerV1) increaseTotalDelegatedAmount(_ int, rlm realm, amount int64) error {
	currentDelegated := gs.store.GetTotalDelegatedAmount()

	if err := gs.store.SetTotalDelegatedAmount(0, rlm, gnsmath.SafeAddInt64(currentDelegated, amount)); err != nil {
		return err
	}

	return nil
}

func (gs *govStakerV1) decreaseTotalDelegatedAmount(_ int, rlm realm, amount int64) error {
	currentDelegated := gs.store.GetTotalDelegatedAmount()

	newDelegated := gnsmath.SafeSubInt64(currentDelegated, amount)
	if newDelegated < 0 {
		newDelegated = 0
	}
	if err := gs.store.SetTotalDelegatedAmount(0, rlm, newDelegated); err != nil {
		return err
	}

	return nil
}

func (gs *govStakerV1) increaseTotalLockedAmount(_ int, rlm realm, amount int64) error {
	currentLocked := gs.store.GetTotalLockedAmount()

	if err := gs.store.SetTotalLockedAmount(0, rlm, gnsmath.SafeAddInt64(currentLocked, amount)); err != nil {
		return err
	}

	return nil
}

func (gs *govStakerV1) decreaseTotalLockedAmount(_ int, rlm realm, amount int64) error {
	currentLocked := gs.store.GetTotalLockedAmount()

	newLocked := gnsmath.SafeSubInt64(currentLocked, amount)
	if newLocked < 0 {
		newLocked = 0
	}
	if err := gs.store.SetTotalLockedAmount(0, rlm, newLocked); err != nil {
		return err
	}

	return nil
}

// collectDelegations processes collection of undelegated tokens.
// Iterates through user delegations and collects available amounts.
func (gs *govStakerV1) collectDelegations(_ int, rlm realm, user address, currentTime int64) (int64, error) {
	totalCollectedAmount := int64(0)

	delegationTree := gs.getUserDelegations(user)

	var err error
	var idsToRemove []int64
	allDelegations := gs.store.GetAllDelegations()

	// Collect from all available delegations
	delegationTree.Iterate("", "", func(delegatee string, value any) bool {
		delegationIDs, ok := value.([]int64)
		if !ok {
			return false
		}

		if len(delegationIDs) == 0 {
			return false
		}
		for _, id := range delegationIDs {
			delegationRaw := allDelegations.Get(utils.FormatInt(id))
			if delegationRaw == nil {
				continue
			}
			delegation, ok := delegationRaw.(*staker.Delegation)
			if !ok {
				continue
			}

			resolver := NewDelegationResolver(delegation)

			collectedAmount, iErr := resolver.processCollection(currentTime)
			if iErr != nil {
				err = iErr
				return true
			}

			// Simple addition since addToCollectedAmount was removed
			totalCollectedAmount = gnsmath.SafeAddInt64(totalCollectedAmount, collectedAmount)

			// Save updated delegation state after collection
			if resolver.IsEmpty() {
				idsToRemove = append(idsToRemove, delegation.ID())
			} else {
				gs.setDelegation(0, rlm, delegation.ID(), delegation)
			}
		}

		return false
	})

	for _, id := range idsToRemove {
		gs.removeDelegation(0, rlm, id)
	}

	if err != nil {
		return totalCollectedAmount, makeErrorWithDetails(errInvalidAmount, err.Error())
	}

	return totalCollectedAmount, nil
}