base.gno

package router

import (
	"errors"
	"strings"

	gnsmath "gno.land/p/gnoswap/gnsmath"
	prbac "gno.land/p/gnoswap/rbac"
	u256 "gno.land/p/gnoswap/uint256"
	ufmt "gno.land/p/nt/ufmt/v0"

	"gno.land/r/gnoswap/access"
)

const (
	SINGLE_HOP_ROUTE int = 1
)

// swap can be done by multiple pools
// to separate each pool, we use POOL_SEPARATOR
const (
	POOL_SEPARATOR = "*POOL*"
)

type RouterOperation interface {
	Validate() error
	Process(_ int, rlm realm) (*SwapResult, error)
}

// executeSwapOperation validates and processes a swap operation.
func executeSwapOperation(_ int, rlm realm, op RouterOperation) (*SwapResult, error) {
	if err := op.Validate(); err != nil {
		return nil, err
	}

	result, err := op.Process(0, rlm)
	if err != nil {
		return nil, err
	}

	return result, nil
}

type BaseSwapParams struct {
	InputToken        string
	OutputToken       string
	RouteArr          string
	QuoteArr          string
	SqrtPriceLimitX96 *u256.Uint
	Deadline          int64
}

// common swap operation
type baseSwapOperation struct {
	sqrtPriceLimitX96 *u256.Uint
	routes            []string
	quotes            []string
	amountSpecified   int64
}

// processRoutes processes all swap routes and returns total amounts.
func (op *baseSwapOperation) processRoutes(_ int, rlm realm, r *routerV1, swapType SwapType) (int64, int64, error) {
	resultAmountIn, resultAmountOut := int64(0), int64(0)
	remainRequestAmount := op.amountSpecified

	for i, route := range op.routes {
		toSwapAmount := int64(0)

		// if it's the last route, use the remaining amount
		isLastRoute := i == len(op.routes)-1
		if !isLastRoute {
			// calculate the amount to swap for this route
			swapAmount, err := calculateSwapAmountByQuote(op.amountSpecified, op.quotes[i])
			if err != nil {
				return 0, 0, err
			}

			// update the remaining amount
			remainRequestAmount = gnsmath.SafeSubInt64(remainRequestAmount, swapAmount)
			toSwapAmount = swapAmount
		} else {
			toSwapAmount = remainRequestAmount
		}

		amountIn, amountOut, err := op.processRoute(0, rlm, r, route, toSwapAmount, swapType)
		if err != nil {
			return 0, 0, err
		}

		resultAmountIn = gnsmath.SafeAddInt64(resultAmountIn, amountIn)
		resultAmountOut = gnsmath.SafeAddInt64(resultAmountOut, amountOut)
	}

	return resultAmountIn, resultAmountOut, nil
}

// processRoute processes a single route with specified swap amount.
func (op *baseSwapOperation) processRoute(
	_ int,
	rlm realm,
	r *routerV1,
	route string,
	toSwap int64,
	swapType SwapType,
) (amountIn, amountOut int64, err error) {
	numHops := strings.Count(route, POOL_SEPARATOR) + 1
	assertHopsInRange(numHops)

	switch numHops {
	case SINGLE_HOP_ROUTE:
		amountIn, amountOut = r.handleSingleSwap(0, rlm, route, toSwap, op.sqrtPriceLimitX96)
	default:
		amountIn, amountOut = r.handleMultiSwap(0, rlm, swapType, route, numHops, toSwap)
	}

	return amountIn, amountOut, nil
}

// handleSingleSwap executes a single-hop swap with the specified amount.
func (r *routerV1) handleSingleSwap(_ int, rlm realm, route string, amountSpecified int64, sqrtPriceLimitX96 *u256.Uint) (int64, int64) {
	input, output, fee := getDataForSinglePath(route)
	singleParams := SingleSwapParams{
		tokenIn:           input,
		tokenOut:          output,
		fee:               fee,
		amountSpecified:   amountSpecified,
		sqrtPriceLimitX96: sqrtPriceLimitX96,
	}

	return r.singleSwap(0, rlm, &singleParams)
}

// handleMultiSwap processes multi-hop swaps across multiple pools.
func (r *routerV1) handleMultiSwap(
	_ int,
	rlm realm,
	swapType SwapType,
	route string,
	numHops int,
	amountSpecified int64,
) (int64, int64) {
	recipient := access.MustGetAddress(prbac.ROLE_ROUTER.String())

	switch swapType {
	case ExactIn:
		input, output, fee := getDataForMultiPath(route, 0) // first data
		sp := newSwapParams(input, output, fee, recipient, amountSpecified)
		return r.multiSwap(0, rlm, *sp, numHops, route)
	case ExactOut:
		input, output, fee := getDataForMultiPath(route, numHops-1) // last data
		sp := newSwapParams(input, output, fee, recipient, amountSpecified)
		return r.multiSwapNegative(0, rlm, *sp, numHops, route)
	default:
		panic(errors.New(errInvalidSwapType))
	}
}

// SwapRouteParams contains all parameters needed for swap route execution
type SwapRouteParams struct {
	inputToken        string
	outputToken       string
	routeArr          string
	quoteArr          string
	deadline          int64
	typ               SwapType
	exactAmount       int64      // amountIn for ExactIn, amountOut for ExactOut
	limitAmount       int64      // amountOutMin for ExactIn, amountInMax for ExactOut
	sqrtPriceLimitX96 *u256.Uint // if sqrtPriceLimitX96 is zero string, it will be set to MIN_PRICE or MAX_PRICE
}

func (p *SwapRouteParams) ExactAmount() int64 {
	return p.exactAmount
}

// when exact out, calculate amount to fetch from pool including router fee
func (p *SwapRouteParams) ExpectedExactAmountByFee(feeBps uint64) int64 {
	if p.typ == ExactIn {
		return p.exactAmount
	}

	return calculateExactOutWithRouterFee(p.exactAmount, feeBps)
}

func (p *SwapRouteParams) SwapCount() int64 {
	swapCount := int64(0)

	for _, route := range strings.Split(p.routeArr, ",") {
		swapCount += int64(strings.Count(route, POOL_SEPARATOR) + 1)
	}

	return swapCount
}

func buildRouteEventAttrs(routeArr string) []string {
	routes := strings.Split(routeArr, ",")
	routeEventAttrs := make([]string, 0, len(routes)*2)

	for index, route := range routes {
		routeEventAttrs = append(routeEventAttrs, ufmt.Sprintf("routes[%d]", index))
		routeEventAttrs = append(routeEventAttrs, route)
	}

	return routeEventAttrs
}

func (p *SwapRouteParams) IsSetSqrtPriceLimitX96() bool {
	return p.sqrtPriceLimitX96 != nil && !p.sqrtPriceLimitX96.IsZero()
}

// createSwapOperation creates the appropriate swap operation based on swap type.
func createSwapOperation(r *routerV1, params SwapRouteParams) (RouterOperation, error) {
	baseParams := BaseSwapParams{
		InputToken:        params.inputToken,
		OutputToken:       params.outputToken,
		RouteArr:          params.routeArr,
		QuoteArr:          params.quoteArr,
		SqrtPriceLimitX96: params.sqrtPriceLimitX96,
		Deadline:          params.deadline,
	}

	switch params.typ {
	case ExactIn:
		pp := NewExactInParams(baseParams, params.ExactAmount(), params.limitAmount)
		return NewExactInSwapOperation(r, pp), nil
	case ExactOut:
		routerFee := r.store.GetSwapFee()
		pp := NewExactOutParams(baseParams, params.ExpectedExactAmountByFee(routerFee), params.limitAmount)
		return NewExactOutSwapOperation(r, pp), nil
	default:
		msg := addDetailToError(errInvalidSwapType, "unknown swap type")
		return nil, errors.New(msg)
	}
}

// commonSwapRoute handles the common logic for both ExactIn and ExactOut swaps.
func (r *routerV1) commonSwapRoute(_ int, rlm realm, params SwapRouteParams) (int64, int64, error) {
	op, err := createSwapOperation(r, params)
	if err != nil {
		return 0, 0, err
	}

	result, err := executeSwapOperation(0, rlm, op)
	if err != nil {
		msg := addDetailToError(
			errInvalidInput,
			ufmt.Sprintf("invalid %s SwapOperation: %s", params.typ.String(), err.Error()),
		)
		return 0, 0, errors.New(msg)
	}

	inputAmount, outputAmount := r.finalizeSwap(
		0,
		rlm,
		params.inputToken,
		params.outputToken,
		result.AmountIn,
		result.AmountOut,
		params.typ,
		params.limitAmount,
		params.ExactAmount(),
		params.SwapCount(),
		params.IsSetSqrtPriceLimitX96(),
	)

	return inputAmount, outputAmount, nil
}