package types

import (
	"fmt"
	"time"

	"github.com/slack-go/slack"

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

var Two = fixedpoint.NewFromInt(2)
var Three = fixedpoint.NewFromInt(3)

type Direction int

const DirectionUp = 1
const DirectionNone = 0
const DirectionDown = -1

type KLineOrWindow interface {
	GetInterval() string
	Direction() Direction
	GetChange() fixedpoint.Value
	GetMaxChange() fixedpoint.Value
	GetThickness() fixedpoint.Value

	Mid() fixedpoint.Value
	GetOpen() fixedpoint.Value
	GetClose() fixedpoint.Value
	GetHigh() fixedpoint.Value
	GetLow() fixedpoint.Value

	BounceUp() bool
	BounceDown() bool
	GetUpperShadowRatio() fixedpoint.Value
	GetLowerShadowRatio() fixedpoint.Value

	SlackAttachment() slack.Attachment
}

type KLineQueryOptions struct {
	Limit     int
	StartTime *time.Time
	EndTime   *time.Time
}

// KLine uses binance's kline as the standard structure
type KLine struct {
	GID      uint64       `json:"gid" db:"gid"`
	Exchange ExchangeName `json:"exchange" db:"exchange"`

	Symbol string `json:"symbol" db:"symbol"`

	StartTime Time `json:"startTime" db:"start_time"`
	// EndTime follows the binance rule, to avoid endTime overlapping with the next startTime. So if your end time (2023-01-01 01:00:00)
	// are overlapping with next start time interval (2023-01-01 01:00:00), you should subtract -1 time.millisecond on EndTime.
	EndTime Time `json:"endTime" db:"end_time"`

	Interval Interval `json:"interval" db:"interval"`

	Open                     fixedpoint.Value `json:"open" db:"open"`
	Close                    fixedpoint.Value `json:"close" db:"close"`
	High                     fixedpoint.Value `json:"high" db:"high"`
	Low                      fixedpoint.Value `json:"low" db:"low"`
	Volume                   fixedpoint.Value `json:"volume" db:"volume"`
	QuoteVolume              fixedpoint.Value `json:"quoteVolume" db:"quote_volume"`
	TakerBuyBaseAssetVolume  fixedpoint.Value `json:"takerBuyBaseAssetVolume" db:"taker_buy_base_volume"`
	TakerBuyQuoteAssetVolume fixedpoint.Value `json:"takerBuyQuoteAssetVolume" db:"taker_buy_quote_volume"`

	LastTradeID    uint64 `json:"lastTradeID" db:"last_trade_id"`
	NumberOfTrades uint64 `json:"numberOfTrades" db:"num_trades"`
	Closed         bool   `json:"closed" db:"closed"`
}

func (k *KLine) ObjectID() string {
	return "kline-" + k.Symbol + k.Interval.String() + k.StartTime.Time().Format(time.RFC3339)
}

func (k *KLine) Set(o *KLine) {
	k.GID = o.GID
	k.Exchange = o.Exchange
	k.Symbol = o.Symbol
	k.StartTime = o.StartTime
	k.EndTime = o.EndTime
	k.Interval = o.Interval
	k.Open = o.Open
	k.Close = o.Close
	k.High = o.High
	k.Low = o.Low
	k.Volume = o.Volume
	k.QuoteVolume = o.QuoteVolume
	k.TakerBuyBaseAssetVolume = o.TakerBuyBaseAssetVolume
	k.TakerBuyQuoteAssetVolume = o.TakerBuyQuoteAssetVolume
	k.LastTradeID = o.LastTradeID
	k.NumberOfTrades = o.NumberOfTrades
	k.Closed = o.Closed
}

func (k *KLine) Merge(o *KLine) {
	k.EndTime = o.EndTime
	k.Close = o.Close
	k.High = fixedpoint.Max(k.High, o.High)
	k.Low = fixedpoint.Min(k.Low, o.Low)
	k.Volume = k.Volume.Add(o.Volume)
	k.QuoteVolume = k.QuoteVolume.Add(o.QuoteVolume)
	k.TakerBuyBaseAssetVolume = k.TakerBuyBaseAssetVolume.Add(o.TakerBuyBaseAssetVolume)
	k.TakerBuyQuoteAssetVolume = k.TakerBuyQuoteAssetVolume.Add(o.TakerBuyQuoteAssetVolume)
	k.LastTradeID = o.LastTradeID
	k.NumberOfTrades += o.NumberOfTrades
	k.Closed = o.Closed
}

func (k *KLine) GetStartTime() Time {
	return k.StartTime
}

func (k *KLine) GetEndTime() Time {
	return k.EndTime
}

func (k *KLine) GetInterval() Interval {
	return k.Interval
}

func (k *KLine) Mid() fixedpoint.Value {
	return k.High.Add(k.Low).Div(Two)
}

// green candle with open and close near high price
func (k *KLine) BounceUp() bool {
	mid := k.Mid()
	trend := k.Direction()
	return trend > 0 && k.Open.Compare(mid) > 0 && k.Close.Compare(mid) > 0
}

// red candle with open and close near low price
func (k *KLine) BounceDown() bool {
	mid := k.Mid()
	trend := k.Direction()
	return trend > 0 && k.Open.Compare(mid) < 0 && k.Close.Compare(mid) < 0
}

func (k *KLine) Direction() Direction {
	o := k.GetOpen()
	c := k.GetClose()

	if c.Compare(o) > 0 {
		return DirectionUp
	} else if c.Compare(o) < 0 {
		return DirectionDown
	}
	return DirectionNone
}

func (k *KLine) GetHigh() fixedpoint.Value {
	return k.High
}

func (k *KLine) GetLow() fixedpoint.Value {
	return k.Low
}

func (k *KLine) GetOpen() fixedpoint.Value {
	return k.Open
}

func (k *KLine) GetClose() fixedpoint.Value {
	return k.Close
}

func (k *KLine) GetMaxChange() fixedpoint.Value {
	return k.GetHigh().Sub(k.GetLow())
}

func (k *KLine) GetAmplification() fixedpoint.Value {
	return k.GetMaxChange().Div(k.GetLow())
}

// GetThickness returns the thickness of the kline. 1 => thick, 0.1 => thin
func (k *KLine) GetThickness() fixedpoint.Value {
	out := k.GetChange().Div(k.GetMaxChange())
	if out.Sign() < 0 {
		return out.Neg()
	}
	return out
}

func (k *KLine) GetUpperShadowRatio() fixedpoint.Value {
	out := k.GetUpperShadowHeight().Div(k.GetMaxChange())
	if out.Sign() < 0 {
		return out.Neg()
	}
	return out
}

func (k *KLine) GetUpperShadowHeight() fixedpoint.Value {
	high := k.GetHigh()
	open := k.GetOpen()
	clos := k.GetClose()
	if open.Compare(clos) > 0 {
		return high.Sub(open)
	}
	return high.Sub(clos)
}

func (k *KLine) GetLowerShadowRatio() fixedpoint.Value {
	out := k.GetLowerShadowHeight().Div(k.GetMaxChange())
	if out.Sign() < 0 {
		return out.Neg()
	}
	return out
}

func (k *KLine) GetLowerShadowHeight() fixedpoint.Value {
	low := k.Low
	if k.Open.Compare(k.Close) < 0 { // uptrend
		return k.Open.Sub(low)
	}

	// downtrend
	return k.Close.Sub(low)
}

// GetBody returns the height of the candle real body
func (k *KLine) GetBody() fixedpoint.Value {
	return k.GetChange()
}

// GetChange returns Close price - Open price.
func (k *KLine) GetChange() fixedpoint.Value {
	return k.Close.Sub(k.Open)
}

func (k *KLine) Color() string {
	if k.Direction() > 0 {
		return style.GreenColor
	} else if k.Direction() < 0 {
		return style.RedColor
	}
	return style.GrayColor
}

func (k *KLine) String() string {
	return fmt.Sprintf("%s %s %s %s O: %.4f H: %.4f L: %.4f C: %.4f CHG: %.4f MAXCHG: %.4f V: %.4f QV: %.2f TBBV: %.2f",
		k.Exchange.String(),
		k.StartTime.Time().Format("2006-01-02 15:04"),
		k.Symbol, k.Interval, k.Open.Float64(), k.High.Float64(), k.Low.Float64(), k.Close.Float64(), k.GetChange().Float64(), k.GetMaxChange().Float64(), k.Volume.Float64(), k.QuoteVolume.Float64(), k.TakerBuyBaseAssetVolume.Float64())
}

func (k *KLine) PlainText() string {
	return k.String()
}

func (k *KLine) SlackAttachment() slack.Attachment {
	return slack.Attachment{
		Text:  fmt.Sprintf("*%s* KLine %s", k.Symbol, k.Interval),
		Color: k.Color(),
		Fields: []slack.AttachmentField{
			{Title: "Open", Value: k.Open.FormatString(2), Short: true},
			{Title: "High", Value: k.High.FormatString(2), Short: true},
			{Title: "Low", Value: k.Low.FormatString(2), Short: true},
			{Title: "Close", Value: k.Close.FormatString(2), Short: true},
			{Title: "Mid", Value: k.Mid().FormatString(2), Short: true},
			{Title: "Change", Value: k.GetChange().FormatString(2), Short: true},
			{Title: "Volume", Value: k.Volume.FormatString(2), Short: true},
			{Title: "Taker Buy Base Volume", Value: k.TakerBuyBaseAssetVolume.FormatString(2), Short: true},
			{Title: "Taker Buy Quote Volume", Value: k.TakerBuyQuoteAssetVolume.FormatString(2), Short: true},
			{Title: "Max Change", Value: k.GetMaxChange().FormatString(2), Short: true},
			{
				Title: "Thickness",
				Value: k.GetThickness().FormatString(4),
				Short: true,
			},
			{
				Title: "UpperShadowRatio",
				Value: k.GetUpperShadowRatio().FormatString(4),
				Short: true,
			},
			{
				Title: "LowerShadowRatio",
				Value: k.GetLowerShadowRatio().FormatString(4),
				Short: true,
			},
		},

		FooterIcon: ExchangeFooterIcon(k.Exchange),
		Footer:     k.StartTime.String() + " ~ " + k.EndTime.String(),
	}
}

type KLineWindow []KLine

// ReduceClose reduces the closed prices
func (k KLineWindow) ReduceClose() fixedpoint.Value {
	s := fixedpoint.Zero

	for _, kline := range k {
		s = s.Add(kline.GetClose())
	}

	return s
}

func (k KLineWindow) Len() int {
	return len(k)
}

func (k KLineWindow) First() KLine {
	return k[0]
}

func (k KLineWindow) Last() KLine {
	return k[len(k)-1]
}

func (k KLineWindow) GetInterval() Interval {
	return k.First().Interval
}

func (k KLineWindow) GetOpen() fixedpoint.Value {
	first := k.First()
	return first.GetOpen()
}

func (k KLineWindow) GetClose() fixedpoint.Value {
	end := len(k) - 1
	return k[end].GetClose()
}

func (k KLineWindow) GetHigh() fixedpoint.Value {
	first := k.First()
	high := first.GetHigh()
	for _, line := range k {
		high = fixedpoint.Max(high, line.GetHigh())
	}

	return high
}

func (k KLineWindow) GetLow() fixedpoint.Value {
	first := k.First()
	low := first.GetLow()
	for _, line := range k {
		low = fixedpoint.Min(low, line.GetLow())
	}

	return low
}

func (k KLineWindow) GetChange() fixedpoint.Value {
	return k.GetClose().Sub(k.GetOpen())
}

func (k KLineWindow) GetMaxChange() fixedpoint.Value {
	return k.GetHigh().Sub(k.GetLow())
}

func (k KLineWindow) GetAmplification() fixedpoint.Value {
	return k.GetMaxChange().Div(k.GetLow())
}

func (k KLineWindow) AllDrop() bool {
	for _, n := range k {
		if n.Direction() >= 0 {
			return false
		}
	}
	return true
}

func (k KLineWindow) AllRise() bool {
	for _, n := range k {
		if n.Direction() <= 0 {
			return false
		}
	}
	return true
}

func (k KLineWindow) GetTrend() int {
	o := k.GetOpen()
	c := k.GetClose()

	if c.Compare(o) > 0 {
		return 1
	} else if c.Compare(o) < 0 {
		return -1
	}
	return 0
}

func (k KLineWindow) Color() string {
	if k.GetTrend() > 0 {
		return style.GreenColor
	} else if k.GetTrend() < 0 {
		return style.RedColor
	}
	return style.GrayColor
}

// Mid price
func (k KLineWindow) Mid() fixedpoint.Value {
	return k.GetHigh().Add(k.GetLow()).Div(Two)
}

// BounceUp returns true if it's green candle with open and close near high price
func (k KLineWindow) BounceUp() bool {
	mid := k.Mid()
	trend := k.GetTrend()
	return trend > 0 && k.GetOpen().Compare(mid) > 0 && k.GetClose().Compare(mid) > 0
}

// BounceDown returns true red candle with open and close near low price
func (k KLineWindow) BounceDown() bool {
	mid := k.Mid()
	trend := k.GetTrend()
	return trend > 0 && k.GetOpen().Compare(mid) < 0 && k.GetClose().Compare(mid) < 0
}

func (k *KLineWindow) Add(line KLine) {
	*k = append(*k, line)
}

func (k KLineWindow) Take(size int) KLineWindow {
	return k[:size]
}

func (k KLineWindow) Tail(size int) KLineWindow {
	length := len(k)
	if length <= size {
		win := make(KLineWindow, length)
		copy(win, k)
		return win
	}

	win := make(KLineWindow, size)
	copy(win, k[length-size:])
	return win
}

// Truncate removes the old klines from the window
func (k *KLineWindow) Truncate(size int) {
	if len(*k) <= size {
		return
	}

	end := len(*k)
	start := max(end-size, 0)
	kn := (*k)[start:]
	*k = kn
}

func (k KLineWindow) GetBody() fixedpoint.Value {
	return k.GetChange()
}

func (k KLineWindow) GetThickness() fixedpoint.Value {
	out := k.GetChange().Div(k.GetMaxChange())
	if out.Sign() < 0 {
		return out.Neg()
	}
	return out
}

func (k KLineWindow) GetUpperShadowRatio() fixedpoint.Value {
	out := k.GetUpperShadowHeight().Div(k.GetMaxChange())
	if out.Sign() < 0 {
		return out.Neg()
	}
	return out
}

func (k KLineWindow) GetUpperShadowHeight() fixedpoint.Value {
	high := k.GetHigh()
	open := k.GetOpen()
	clos := k.GetClose()
	if open.Compare(clos) > 0 {
		return high.Sub(open)
	}
	return high.Sub(clos)
}

func (k KLineWindow) GetLowerShadowRatio() fixedpoint.Value {
	out := k.GetLowerShadowHeight().Div(k.GetMaxChange())
	if out.Sign() < 0 {
		return out.Neg()
	}
	return out
}

func (k KLineWindow) GetLowerShadowHeight() fixedpoint.Value {
	low := k.GetLow()
	open := k.GetOpen()
	clos := k.GetClose()
	if open.Compare(clos) < 0 {
		return open.Sub(low)
	}
	return clos.Sub(low)
}

func (k KLineWindow) SlackAttachment() slack.Attachment {
	var first KLine
	var end KLine
	var windowSize = len(k)
	if windowSize > 0 {
		first = k[0]
		end = k[windowSize-1]
	}

	return slack.Attachment{
		Text:  fmt.Sprintf("*%s* KLineWindow %s x %d", first.Symbol, first.Interval, windowSize),
		Color: k.Color(),
		Fields: []slack.AttachmentField{
			{Title: "Open", Value: k.GetOpen().FormatString(2), Short: true},
			{Title: "High", Value: k.GetHigh().FormatString(2), Short: true},
			{Title: "Low", Value: k.GetLow().FormatString(2), Short: true},
			{Title: "Close", Value: k.GetClose().FormatString(2), Short: true},
			{Title: "Mid", Value: k.Mid().FormatPercentage(2), Short: true},
			{
				Title: "Change",
				Value: k.GetChange().FormatString(2),
				Short: true,
			},
			{
				Title: "Max Change",
				Value: k.GetMaxChange().FormatString(2),
				Short: true,
			},
			{
				Title: "Thickness",
				Value: k.GetThickness().FormatString(4),
				Short: true,
			},
			{
				Title: "UpperShadowRatio",
				Value: k.GetUpperShadowRatio().FormatString(4),
				Short: true,
			},
			{
				Title: "LowerShadowRatio",
				Value: k.GetLowerShadowRatio().FormatString(4),
				Short: true,
			},
		},
		Footer:     fmt.Sprintf("Since %s til %s", first.StartTime, end.EndTime),
		FooterIcon: "",
	}
}

type KLineCallback func(k KLine)

type KValueType int

const (
	kOpUnknown KValueType = iota
	kOpenValue
	kCloseValue
	kHighValue
	kLowValue
	kVolumeValue
)

func (k *KLineWindow) High() Series {
	return &KLineSeries{
		lines: k,
		kv:    kHighValue,
	}
}

func (k *KLineWindow) Low() Series {
	return &KLineSeries{
		lines: k,
		kv:    kLowValue,
	}
}

func (k *KLineWindow) Open() Series {
	return &KLineSeries{
		lines: k,
		kv:    kOpenValue,
	}
}

func (k *KLineWindow) Close() Series {
	return &KLineSeries{
		lines: k,
		kv:    kCloseValue,
	}
}

func (k *KLineWindow) Volume() Series {
	return &KLineSeries{
		lines: k,
		kv:    kVolumeValue,
	}
}

type KLineSeries struct {
	lines *KLineWindow
	kv    KValueType
}

func (k *KLineSeries) Index(i int) float64 {
	return k.Last(i)
}

func (k *KLineSeries) Last(i int) float64 {
	length := len(*k.lines)
	if length == 0 || length-i-1 < 0 {
		return 0
	}
	switch k.kv {
	case kOpenValue:
		return (*k.lines)[length-i-1].GetOpen().Float64()
	case kCloseValue:
		return (*k.lines)[length-i-1].GetClose().Float64()
	case kLowValue:
		return (*k.lines)[length-i-1].GetLow().Float64()
	case kHighValue:
		return (*k.lines)[length-i-1].GetHigh().Float64()
	case kVolumeValue:
		return (*k.lines)[length-i-1].Volume.Float64()
	}
	return 0
}

func (k *KLineSeries) Length() int {
	return len(*k.lines)
}

var _ Series = &KLineSeries{}

func TradeWith(symbol string, f func(trade Trade)) func(trade Trade) {
	return func(trade Trade) {
		if symbol != "" && trade.Symbol != symbol {
			return
		}

		f(trade)
	}
}

func KLineWith(symbol string, interval Interval, callback KLineCallback) KLineCallback {
	return func(k KLine) {
		if k.Symbol != symbol || (k.Interval != "" && k.Interval != interval) {
			return
		}
		callback(k)
	}
}

type KLineValueMapper func(k KLine) float64

func KLineOpenPriceMapper(k KLine) float64 {
	return k.Open.Float64()
}

func KLineClosePriceMapper(k KLine) float64 {
	return k.Close.Float64()
}

func KLineTypicalPriceMapper(k KLine) float64 {
	return (k.High.Float64() + k.Low.Float64() + k.Close.Float64()) / 3.
}

func KLinePriceVolumeMapper(k KLine) float64 {
	return k.Close.Mul(k.Volume).Float64()
}

func KLineVolumeMapper(k KLine) float64 {
	return k.Volume.Float64()
}

func KLineHLC3Mapper(k KLine) float64 {
	return k.High.Add(k.Low).Add(k.Close).Div(Three).Float64()
}

func MapKLinePrice(kLines []KLine, f KLineValueMapper) (prices []float64) {
	for _, k := range kLines {
		prices = append(prices, f(k))
	}

	return prices
}

func KLineLowPriceMapper(k KLine) float64 {
	return k.Low.Float64()
}

func KLineHighPriceMapper(k KLine) float64 {
	return k.High.Float64()
}

// ShrinkSlice shrinks the slice to the new size by removing the old klines
func ShrinkSlice[S ~[]E, E comparable](slice S, thresholdLen, newSize int) S {
	curLen := len(slice)
	curCap := cap(slice)

	// newSize can't be larger than half of the current capacity
	if newSize > curCap/2 {
		newSize = curCap / 2
	}

	if curLen < thresholdLen || curLen <= newSize {
		return slice
	}

	start := curLen - newSize
	newSlice := make(S, newSize)
	copy(newSlice, slice[start:])
	return newSlice
}

// Validate checks the mandatory fields of KLine and returns an error if any are invalid.
// Mandatory fields: Symbol, Interval, Open, Close, High, Low, StartTime, EndTime.
func (k *KLine) Validate() error {
	if k.Symbol == "" {
		return fmt.Errorf("kline symbol is empty")
	}
	if k.Interval == "" {
		return fmt.Errorf("kline interval is empty")
	}
	if k.Open.Sign() <= 0 {
		return fmt.Errorf("kline open price must be positive, got %s", k.Open.String())
	}
	if k.Close.Sign() <= 0 {
		return fmt.Errorf("kline close price must be positive, got %s", k.Close.String())
	}
	if k.High.Sign() <= 0 {
		return fmt.Errorf("kline high price must be positive, got %s", k.High.String())
	}
	if k.Low.Sign() <= 0 {
		return fmt.Errorf("kline low price must be positive, got %s", k.Low.String())
	}

	if time.Time(k.StartTime).IsZero() {
		return fmt.Errorf("kline start time is zero")
	}
	if time.Time(k.EndTime).IsZero() {
		return fmt.Errorf("kline end time is zero")
	}
	return nil
}
