package exmonitor

import (
	"fmt"
	"math/big"
	"sync"
	"time"
)

type KLine struct {
	Time         int64
	Period       string
	Count        int
	OpenPrice    *big.Float
	ClosePrice   *big.Float
	HighestPrice *big.Float
	LowestPrice  *big.Float
	Volume       *big.Float
	Turnover     *big.Float
}

type CoinThumb struct {
	Symbol       string
	Open         *big.Float
	Close        *big.Float
	High         *big.Float
	Low          *big.Float
	Volume       *big.Float
	Turnover     *big.Float
	Change       *big.Float
	Chg          *big.Float
	BaseUsdRate  *big.Float
	UsdRate      *big.Float
	LastDayClose *big.Float
}

type ExchangeTrade struct {
	Price  *big.Float
	Amount *big.Float
}

type MarketHandler interface {
	HandleTrade(symbol string, trade *ExchangeTrade) error
	HandleKLine(symbol string, kline *KLine)
}

type MarketService interface {
	//FindAllKLine(symbol string, start, end int64, period string) []*KLine
	FindTradeVolume(symbol string, start, end int64) *big.Float
	FindTradeByTimeRange(symbol string, start, end int64) []*ExchangeTrade
	FindAllKLineByTimeRange(symbol string, fromTime, toTime int64, period string) []*KLine
	SaveKLine(symbol string, kline *KLine)
}

type DefaultCoinProcessor struct {
	symbol           string
	baseCoin         string
	currentKLine     *KLine
	handlers         []MarketHandler
	coinThumb        *CoinThumb
	service          MarketService
	coinExchangeRate map[string]*big.Float
	isHalt           bool
	stopKLine        bool
	mutex            sync.Mutex
}

type coinProcessorFactory struct {
	mux            sync.Mutex
	coinProcessors map[string]*DefaultCoinProcessor
}

func (cpf *coinProcessorFactory) GetCoinProcessor(symbol, baseCoin string) *DefaultCoinProcessor {
	cpf.mux.Lock()
	defer cpf.mux.Unlock()
	if processor, exists := cpf.coinProcessors[symbol]; exists {
		return processor
	} else {
		processor = &DefaultCoinProcessor{
			symbol:       symbol,
			baseCoin:     baseCoin,
			currentKLine: createNewKLine(),
			handlers:     []MarketHandler{},
			coinThumb:    &CoinThumb{},
			isHalt:       true,
			stopKLine:    false,
		}
		cpf.coinProcessors[symbol] = processor
		return processor
	}
}

func (cpf *coinProcessorFactory) GetProcessorMap() map[string]*DefaultCoinProcessor {
	cpf.mux.Lock()
	defer cpf.mux.Unlock()
	// copy the map to avoid concurrent map writes
	// and return a new map
	processorMap := make(map[string]*DefaultCoinProcessor)
	for k, v := range cpf.coinProcessors {
		processorMap[k] = v
	}
	return processorMap
}

func createNewKLine() *KLine {
	now := time.Now()
	nextMinute := now.Add(time.Minute)
	return &KLine{
		Time:         nextMinute.UnixMilli(),
		Period:       "1min",
		Count:        0,
		OpenPrice:    big.NewFloat(0),
		ClosePrice:   big.NewFloat(0),
		HighestPrice: big.NewFloat(0),
		LowestPrice:  big.NewFloat(0),
		Volume:       big.NewFloat(0),
		Turnover:     big.NewFloat(0),
	}
}

func (p *DefaultCoinProcessor) InitializeThumb() {
	p.mutex.Lock()
	defer p.mutex.Unlock()

	now := time.Now()
	startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	lines := p.service.FindAllKLineByTimeRange(p.symbol, startOfDay.UnixMilli(), now.UnixMilli(), "1min")

	p.coinThumb = &CoinThumb{
		Symbol:   p.symbol,
		Open:     big.NewFloat(0),
		High:     big.NewFloat(0),
		Low:      big.NewFloat(0),
		Close:    big.NewFloat(0),
		Volume:   big.NewFloat(0),
		Turnover: big.NewFloat(0),
	}

	for _, line := range lines {
		if line.OpenPrice.Cmp(big.NewFloat(0)) == 0 {
			continue
		}
		if p.coinThumb.Open.Cmp(big.NewFloat(0)) == 0 {
			p.coinThumb.Open = line.OpenPrice
		}
		if p.coinThumb.High.Cmp(line.HighestPrice) < 0 {
			p.coinThumb.High = line.HighestPrice
		}
		if line.LowestPrice.Cmp(big.NewFloat(0)) > 0 && p.coinThumb.Low.Cmp(line.LowestPrice) > 0 {
			p.coinThumb.Low = line.LowestPrice
		}
		if line.ClosePrice.Cmp(big.NewFloat(0)) > 0 {
			p.coinThumb.Close = line.ClosePrice
		}
		p.coinThumb.Volume.Add(p.coinThumb.Volume, line.Volume)
		p.coinThumb.Turnover.Add(p.coinThumb.Turnover, line.Turnover)
	}
	change := new(big.Float).Sub(p.coinThumb.Close, p.coinThumb.Open)
	p.coinThumb.Change = change
	if p.coinThumb.Low.Cmp(big.NewFloat(0)) > 0 {
		p.coinThumb.Chg = new(big.Float).Quo(change, p.coinThumb.Low)
	}
}

func (p *DefaultCoinProcessor) IsStopKline() bool {
	return p.stopKLine
}

func (p *DefaultCoinProcessor) ResetThumb() {
	p.mutex.Lock()
	defer p.mutex.Unlock()

	p.coinThumb.Open = big.NewFloat(0)
	p.coinThumb.High = big.NewFloat(0)
	p.coinThumb.Low = big.NewFloat(0)
	p.coinThumb.Close = big.NewFloat(0)
	p.coinThumb.Change = big.NewFloat(0)
	p.coinThumb.Chg = big.NewFloat(0)
	p.coinThumb.LastDayClose = p.coinThumb.Close
}

func (p *DefaultCoinProcessor) AutoGenerate() {
	p.mutex.Lock()
	defer p.mutex.Unlock()

	if p.coinThumb != nil {
		if p.currentKLine.OpenPrice.Cmp(big.NewFloat(0)) == 0 {
			p.currentKLine.OpenPrice = p.coinThumb.Close
			p.currentKLine.LowestPrice = p.coinThumb.Close
			p.currentKLine.HighestPrice = p.coinThumb.Close
			p.currentKLine.ClosePrice = p.coinThumb.Close
		}
		p.currentKLine.Time = time.Now().UnixMilli()
		p.handleKLineStorage(p.currentKLine)
		p.currentKLine = createNewKLine()
	}
}

func (p *DefaultCoinProcessor) handleKLineStorage(kline *KLine) {
	for _, handler := range p.handlers {
		handler.HandleKLine(p.symbol, kline)
	}
}

func (p *DefaultCoinProcessor) AddHandler(handler MarketHandler) {
	p.handlers = append(p.handlers, handler)
}

func (p *DefaultCoinProcessor) GenerateKLine(rangeValue int, field time.Duration, timestamp int64) {
	p.stopKLine = false
	defer func() { p.stopKLine = true }()
	endTime := time.UnixMilli(timestamp)
	startTime := endTime.Add(-field * time.Duration(rangeValue))
	trades := p.service.FindTradeByTimeRange(p.symbol, startTime.UnixMilli(), endTime.UnixMilli())

	kline := &KLine{
		Time:         endTime.UnixMilli(),
		Period:       formatPeriod(rangeValue, field),
		OpenPrice:    big.NewFloat(0),
		ClosePrice:   big.NewFloat(0),
		HighestPrice: big.NewFloat(0),
		LowestPrice:  big.NewFloat(0),
		Volume:       big.NewFloat(0),
		Turnover:     big.NewFloat(0),
	}

	for _, trade := range trades {
		p.processTrade(kline, trade)
	}

	if kline.OpenPrice.Cmp(big.NewFloat(0)) == 0 {
		kline.OpenPrice = p.coinThumb.Close
		kline.ClosePrice = p.coinThumb.Close
		kline.LowestPrice = p.coinThumb.Close
		kline.HighestPrice = p.coinThumb.Close
	}

	p.service.SaveKLine(p.symbol, kline)
}

func (p *DefaultCoinProcessor) processTrade(kline *KLine, trade *ExchangeTrade) {
	if kline.OpenPrice.Cmp(big.NewFloat(0)) == 0 {
		kline.OpenPrice = trade.Price
		kline.HighestPrice = trade.Price
		kline.LowestPrice = trade.Price
		kline.ClosePrice = trade.Price
	} else {
		if trade.Price.Cmp(kline.HighestPrice) > 0 {
			kline.HighestPrice = trade.Price
		}
		if trade.Price.Cmp(kline.LowestPrice) < 0 {
			kline.LowestPrice = trade.Price
		}
		kline.ClosePrice = trade.Price
	}
	kline.Count++
	kline.Volume.Add(kline.Volume, trade.Amount)
	turnover := new(big.Float).Mul(trade.Price, trade.Amount)
	kline.Turnover.Add(kline.Turnover, turnover)
}

func formatPeriod(rangeValue int, field time.Duration) string {
	switch field {
	case K_FIELD_MIN:
		return fmt.Sprintf("%dmin", rangeValue)
	case K_FIELD_HOUR:
		return fmt.Sprintf("%dhour", rangeValue)
	case K_FIELD_DAY:
		return fmt.Sprintf("%dday", rangeValue)
	case K_FIELD_WEEK:
		return fmt.Sprintf("%dweek", rangeValue)
	case K_FIELD_MONTH:
		return fmt.Sprintf("%dmonth", rangeValue)
	case K_FIELD_YEAR:
		return fmt.Sprintf("%dyear", rangeValue)
	default:
		return "unknown"
	}
}

func (p *DefaultCoinProcessor) Update24HVolume(currentTime int64) {
	if p.coinThumb != nil {
		p.mutex.Lock()
		defer p.mutex.Unlock()

		// Calculate the start time (24 hours ago)
		startTime := time.UnixMilli(currentTime).Add(-24 * time.Hour).UnixMilli()

		// Fetch the trade volume from the service
		volume := p.service.FindTradeVolume(p.symbol, startTime, currentTime)

		// Set the volume in the coinThumb, rounded to 4 decimal places
		p.coinThumb.Volume = new(big.Float).SetPrec(4).SetMode(big.ToZero).Set(volume)
	}
}
