package autobuy

import (
	"context"
	"fmt"
	"sync"

	"github.com/robfig/cron/v3"
	"github.com/sirupsen/logrus"

	"github.com/c9s/bbgo/pkg/bbgo"
	"github.com/c9s/bbgo/pkg/fixedpoint"
	indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2"
	"github.com/c9s/bbgo/pkg/strategy/common"
	"github.com/c9s/bbgo/pkg/types"
)

const ID = "autobuy"

var log = logrus.WithField("strategy", ID)

func init() {
	bbgo.RegisterStrategy(ID, &Strategy{})
}

type Strategy struct {
	*common.Strategy

	Environment *bbgo.Environment
	Market      types.Market

	Symbol         string                  `json:"symbol"`
	Schedule       string                  `json:"schedule"`
	MinBaseBalance fixedpoint.Value        `json:"minBaseBalance"`
	OrderType      types.OrderType         `json:"orderType"`
	PriceType      types.PriceType         `json:"priceType"`
	Bollinger      *types.BollingerSetting `json:"bollinger"`
	DryRun         bool                    `json:"dryRun"`

	// Deprecated, to be replaced by MinBaseBalance
	Threshold fixedpoint.Value `json:"threshold"`

	bbgo.QuantityOrAmount

	boll *indicatorv2.BOLLStream
	cron *cron.Cron
}

func (s *Strategy) Initialize() error {
	if s.Strategy == nil {
		s.Strategy = &common.Strategy{}
	}

	if s.Threshold.Sign() > 0 && s.MinBaseBalance.IsZero() {
		s.MinBaseBalance = s.Threshold
	}
	return nil
}

func (s *Strategy) ID() string {
	return ID
}

func (s *Strategy) InstanceID() string {
	return fmt.Sprintf("%s:%s", ID, s.Symbol)
}

func (s *Strategy) Validate() error {
	if s.Symbol == "" {
		return fmt.Errorf("symbol is required")
	}

	if s.Schedule == "" {
		return fmt.Errorf("schedule is required")
	}

	if s.MinBaseBalance.Sign() <= 0 {
		return fmt.Errorf("minBaseBalance must be greater than 0")
	}

	if err := s.QuantityOrAmount.Validate(); err != nil {
		return err
	}

	if s.Bollinger != nil {
		if s.Bollinger.Interval == "" {
			return fmt.Errorf("bollinger interval is required")
		}

		if s.Bollinger.BandWidth <= 0 {
			return fmt.Errorf("bollinger band width must be greater than 0")
		}
	}
	return nil
}

func (s *Strategy) Defaults() error {
	if s.OrderType == "" {
		s.OrderType = types.OrderTypeLimit
	}

	if s.PriceType == "" {
		s.PriceType = types.PriceTypeMaker
	}
	return nil
}

func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
	if s.Bollinger != nil {
		session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Bollinger.Interval})
	}
}

func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
	s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID())
	s.Strategy.OrderExecutor.DisableNotify()

	if s.Bollinger != nil {
		s.boll = session.Indicators(s.Symbol).BOLL(s.Bollinger.IntervalWindow, s.Bollinger.BandWidth)
	}

	s.OrderExecutor.ActiveMakerOrders().OnFilled(func(order types.Order) {
		s.autobuy(ctx)
	})

	// the shutdown handler, you can cancel all orders
	bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
		defer wg.Done()
		s.cancelOrders(ctx)
	})

	s.cron = cron.New()
	_, err := s.cron.AddFunc(s.Schedule, func() {
		s.autobuy(ctx)
	})
	if err != nil {
		return err
	}

	s.cron.Start()

	return nil
}

func (s *Strategy) cancelOrders(ctx context.Context) {
	if err := s.OrderExecutor.GracefulCancel(ctx); err != nil {
		log.WithError(err).Errorf("failed to cancel orders")
	}
}

func (s *Strategy) autobuy(ctx context.Context) {
	s.cancelOrders(ctx)

	account := s.Session.GetAccount()
	baseBalance, _ := account.Balance(s.Market.BaseCurrency)

	log.Infof("balance: %s", baseBalance.String())

	quoteBalance, ok := account.Balance(s.Market.QuoteCurrency)
	if !ok {
		log.Errorf("quote balance is zero")
		return
	}

	ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol)
	if err != nil {
		log.WithError(err).Errorf("failed to query ticker")
		return
	}

	side := types.SideTypeBuy
	price := s.PriceType.GetPrice(ticker, side)

	if s.boll != nil && price.Float64() > s.boll.UpBand.Last(0) {
		log.Infof("price %s is higher than upper band %f, skip", price.String(), s.boll.UpBand.Last(0))
		return
	}

	if baseBalance.Available.Compare(s.MinBaseBalance) > 0 {
		log.Infof("balance %s is higher than minBaseBalance %s", baseBalance.Available.String(), s.MinBaseBalance.String())
		return
	}
	log.Infof("balance %s is lower than minBaseBalance %s", baseBalance.Available.String(), s.MinBaseBalance.String())

	quantity := s.CalculateQuantity(price)

	requiredQuote := quantity.Mul(price)
	if quoteBalance.Available.Compare(requiredQuote) < 0 {
		log.Warnf("quote balance %s is not enough: %s < %s", s.Market.QuoteCurrency, quoteBalance.Available.String(), requiredQuote.String())
		return
	}

	submitOrder := types.SubmitOrder{
		Symbol:   s.Symbol,
		Side:     side,
		Type:     s.OrderType,
		Quantity: quantity,
		Price:    price,
	}

	if s.DryRun {
		log.Infof("dry run, skip")
		return
	}

	log.Infof("submitting order: %s", submitOrder.String())
	_, err = s.OrderExecutor.SubmitOrders(ctx, submitOrder)
	if err != nil {
		log.WithError(err).Errorf("submit order error")
	}
}
