package ticker

import (
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/gofrs/uuid"
	"github.com/thrasher-corp/gocryptotrader/currency"
	"github.com/thrasher-corp/gocryptotrader/dispatch"
	"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)

var (
	errInvalidTicker       = errors.New("invalid ticker")
	errTickerNotFound      = errors.New("ticker not found")
	errExchangeNameIsEmpty = errors.New("exchange name is empty")
	errBidEqualsAsk        = errors.New("bid equals ask this is a crossed or locked market")
	errBidGreaterThanAsk   = errors.New("bid greater than ask this is a crossed or locked market")
)

func init() {
	service = new(Service)
	service.Tickers = make(map[string]map[*currency.Item]map[*currency.Item]map[asset.Item]*Ticker)
	service.Exchange = make(map[string]uuid.UUID)
	service.mux = dispatch.GetNewMux(nil)
}

// SubscribeTicker subscribes to a ticker and returns a communication channel to
// stream new ticker updates
func SubscribeTicker(exchange string, p currency.Pair, a asset.Item) (dispatch.Pipe, error) {
	exchange = strings.ToLower(exchange)
	service.mu.Lock()
	defer service.mu.Unlock()

	tick, ok := service.Tickers[exchange][p.Base.Item][p.Quote.Item][a]
	if !ok {
		return dispatch.Pipe{}, fmt.Errorf("ticker item not found for %s %s %s",
			exchange,
			p,
			a)
	}
	return service.mux.Subscribe(tick.Main)
}

// SubscribeToExchangeTickers subscribes to all tickers on an exchange
func SubscribeToExchangeTickers(exchange string) (dispatch.Pipe, error) {
	exchange = strings.ToLower(exchange)
	service.mu.Lock()
	defer service.mu.Unlock()
	id, ok := service.Exchange[exchange]
	if !ok {
		return dispatch.Pipe{}, fmt.Errorf("%s exchange tickers not found",
			exchange)
	}

	return service.mux.Subscribe(id)
}

// GetTicker checks and returns a requested ticker if it exists
func GetTicker(exchange string, p currency.Pair, a asset.Item) (*Price, error) {
	if exchange == "" {
		return nil, errExchangeNameIsEmpty
	}
	if p.IsEmpty() {
		return nil, currency.ErrCurrencyPairEmpty
	}
	if !a.IsValid() {
		return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
	}
	exchange = strings.ToLower(exchange)
	service.mu.Lock()
	defer service.mu.Unlock()
	m1, ok := service.Tickers[exchange]
	if !ok {
		return nil, fmt.Errorf("no tickers for %s exchange", exchange)
	}

	m2, ok := m1[p.Base.Item]
	if !ok {
		return nil, fmt.Errorf("no tickers associated with base currency %s",
			p.Base)
	}

	m3, ok := m2[p.Quote.Item]
	if !ok {
		return nil, fmt.Errorf("no tickers associated with quote currency %s",
			p.Quote)
	}

	t, ok := m3[a]
	if !ok {
		return nil, fmt.Errorf("no tickers associated with asset type %s",
			a)
	}

	cpy := t.Price // Don't let external functions have access to underlying
	return &cpy, nil
}

// FindLast searches for a currency pair and returns the first available
func FindLast(p currency.Pair, a asset.Item) (float64, error) {
	service.mu.Lock()
	defer service.mu.Unlock()
	for _, m1 := range service.Tickers {
		m2, ok := m1[p.Base.Item]
		if !ok {
			continue
		}
		m3, ok := m2[p.Quote.Item]
		if !ok {
			continue
		}
		t, ok := m3[a]
		if !ok {
			continue
		}

		if t.Last == 0 {
			return 0, errInvalidTicker
		}
		return t.Last, nil
	}
	return 0, fmt.Errorf("%w %s %s", errTickerNotFound, p, a)
}

// ProcessTicker processes incoming tickers, creating or updating the Tickers
// list
func ProcessTicker(p *Price) error {
	if p == nil {
		return errors.New(errTickerPriceIsNil)
	}

	if p.ExchangeName == "" {
		return fmt.Errorf(ErrExchangeNameUnset)
	}

	if p.Pair.IsEmpty() {
		return fmt.Errorf("%s %s", p.ExchangeName, errPairNotSet)
	}

	if p.Bid != 0 && p.Ask != 0 {
		switch {
		case p.ExchangeName == "Bitfinex" && p.AssetType == asset.MarginFunding:
		// Margin funding books can be crossed see Bitfinex.
		default:
			if p.Bid == p.Ask {
				return fmt.Errorf("%s %s %w",
					p.ExchangeName,
					p.Pair,
					errBidEqualsAsk)
			}

			if p.Bid > p.Ask {
				return fmt.Errorf("%s %s %w",
					p.ExchangeName,
					p.Pair,
					errBidGreaterThanAsk)
			}
		}
	}

	if p.AssetType == asset.Empty {
		return fmt.Errorf("%s %s %s",
			p.ExchangeName,
			p.Pair,
			errAssetTypeNotSet)
	}

	if p.LastUpdated.IsZero() {
		p.LastUpdated = time.Now()
	}

	return service.update(p)
}

// update updates ticker price
func (s *Service) update(p *Price) error {
	name := strings.ToLower(p.ExchangeName)
	s.mu.Lock()

	m1, ok := service.Tickers[name]
	if !ok {
		m1 = make(map[*currency.Item]map[*currency.Item]map[asset.Item]*Ticker)
		service.Tickers[name] = m1
	}

	m2, ok := m1[p.Pair.Base.Item]
	if !ok {
		m2 = make(map[*currency.Item]map[asset.Item]*Ticker)
		m1[p.Pair.Base.Item] = m2
	}

	m3, ok := m2[p.Pair.Quote.Item]
	if !ok {
		m3 = make(map[asset.Item]*Ticker)
		m2[p.Pair.Quote.Item] = m3
	}

	t, ok := m3[p.AssetType]
	if !ok || t == nil {
		newTicker := &Ticker{}
		err := s.setItemID(newTicker, p, name)
		if err != nil {
			s.mu.Unlock()
			return err
		}
		m3[p.AssetType] = newTicker
		s.mu.Unlock()
		return nil
	}

	t.Price = *p
	//nolint: gocritic
	ids := append(t.Assoc, t.Main)
	s.mu.Unlock()
	return s.mux.Publish(p, ids...)
}

// setItemID retrieves and sets dispatch mux publish IDs
func (s *Service) setItemID(t *Ticker, p *Price, exch string) error {
	ids, err := s.getAssociations(exch)
	if err != nil {
		return err
	}
	singleID, err := s.mux.GetID()
	if err != nil {
		return err
	}

	t.Price = *p
	t.Main = singleID
	t.Assoc = ids
	return nil
}

// getAssociations links a singular book with it's dispatch associations
func (s *Service) getAssociations(exch string) ([]uuid.UUID, error) {
	if exch == "" {
		return nil, errExchangeNameIsEmpty
	}
	var ids []uuid.UUID
	exchangeID, ok := s.Exchange[exch]
	if !ok {
		var err error
		exchangeID, err = s.mux.GetID()
		if err != nil {
			return nil, err
		}
		s.Exchange[exch] = exchangeID
	}
	ids = append(ids, exchangeID)
	return ids, nil
}
