package riskcontrol

import (
	"context"

	log "github.com/sirupsen/logrus"

	"github.com/c9s/bbgo/pkg/bbgo"
	"github.com/c9s/bbgo/pkg/fixedpoint"
	"github.com/c9s/bbgo/pkg/types"
)

// PositionRiskControl controls the position with the given hard limit
// TODO: add a decorator for the order executor and move the order submission logics into the decorator
//
//go:generate callbackgen -type PositionRiskControl
type PositionRiskControl struct {
	orderExecutor bbgo.OrderExecutorExtended

	// hardLimit is the maximum base position you can hold
	hardLimit fixedpoint.Value

	// sliceQuantity is the maximum quantity of the order you want to place.
	// only used in the ModifiedQuantity method
	sliceQuantity fixedpoint.Value

	// activeOrderBook is used to store orders created by the risk control.
	// This allows us to cancel them before submitting the position release
	// orders, preventing duplicate orders.
	activeOrderBook *bbgo.ActiveOrderBook

	releasePositionCallbacks []func(quantity fixedpoint.Value, side types.SideType)
}

func NewPositionRiskControl(orderExecutor bbgo.OrderExecutorExtended, hardLimit, quantity fixedpoint.Value) *PositionRiskControl {
	control := &PositionRiskControl{
		orderExecutor: orderExecutor,
		hardLimit:     hardLimit,
		sliceQuantity: quantity,
	}

	// register position update handler: check if position is over the hard limit
	orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
		if fixedpoint.Compare(position.Base, hardLimit) > 0 {
			log.Infof("position %f is over hardlimit %f, releasing position...", position.Base.Float64(), hardLimit.Float64())
			control.EmitReleasePosition(position.Base.Sub(hardLimit), types.SideTypeSell)
		} else if fixedpoint.Compare(position.Base, hardLimit.Neg()) < 0 {
			log.Infof("position %f is over hardlimit %f, releasing position...", position.Base.Float64(), hardLimit.Float64())
			control.EmitReleasePosition(position.Base.Neg().Sub(hardLimit), types.SideTypeBuy)
		}
	})

	return control
}

func (p *PositionRiskControl) Initialize(ctx context.Context, session *bbgo.ExchangeSession) {
	p.activeOrderBook = bbgo.NewActiveOrderBook("")
	p.activeOrderBook.BindStream(session.UserDataStream)

	p.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) {
		if err := p.activeOrderBook.GracefulCancel(ctx, session.Exchange); err != nil {
			log.WithError(err).Errorf("failed to cancel orders")
		}

		pos := p.orderExecutor.Position()
		submitOrder := types.SubmitOrder{
			Symbol:   pos.Symbol,
			Market:   pos.Market,
			Side:     side,
			Type:     types.OrderTypeMarket,
			Quantity: quantity,
		}

		log.Infof("RiskControl: position limit exceeded, submitting order to reduce position: %+v", submitOrder)
		createdOrders, err := p.orderExecutor.SubmitOrders(ctx, submitOrder)
		if err != nil {
			log.WithError(err).Errorf("failed to submit orders")
			return
		}

		log.Infof("created position release orders: %+v", createdOrders)

		p.activeOrderBook.Add(createdOrders...)
	})
}

// ModifiedQuantity returns sliceQuantity controlled by position risks
// For buy orders, modify sliceQuantity = min(hardLimit - position, sliceQuantity), limiting by positive position
// For sell orders, modify sliceQuantity = min(hardLimit - (-position), sliceQuantity), limiting by negative position
//
// Pass the current base position to this method, and it returns the maximum sliceQuantity for placing the orders.
// This works for both Long/Short position
func (p *PositionRiskControl) ModifiedQuantity(position fixedpoint.Value) (buyQuantity, sellQuantity fixedpoint.Value) {
	if p.sliceQuantity.IsZero() {
		buyQuantity = p.hardLimit.Sub(position)
		sellQuantity = p.hardLimit.Add(position)
		return buyQuantity, sellQuantity
	}

	buyQuantity = fixedpoint.Min(p.hardLimit.Sub(position), p.sliceQuantity)
	sellQuantity = fixedpoint.Min(p.hardLimit.Add(position), p.sliceQuantity)
	return buyQuantity, sellQuantity
}
