package prices

import (
	"context"
	"errors"
	"time"

	"github.com/shopspring/decimal"
	wormscanNotionalCache "github.com/wormhole-foundation/wormhole-explorer/common/client/cache/notional"
	"github.com/wormhole-foundation/wormhole-explorer/common/domain"
	sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
	"go.uber.org/zap"
)

var (
	ErrTokenNotFound = errors.New("token not found")
)

type Price struct {
	CoingeckoID string    `json:"coingeckoId"`
	Symbol      string    `json:"symbol"`
	Price       string    `json:"price"`
	Datetime    time.Time `json:"dateTime"`
}

// PriceService provides an interface to interact with prices.
type PriceService struct {
	priceRepository *PriceRepository
	tokenProvider   *domain.TokenProvider
	notionalCache   wormscanNotionalCache.NotionalLocalCacheReadable
	logger          *zap.Logger
}

// NewPriceService creates a new price service.
func NewPriceService(priceRepository *PriceRepository,
	tokenProvider *domain.TokenProvider,
	notionalCache wormscanNotionalCache.NotionalLocalCacheReadable,
	logger *zap.Logger) *PriceService {
	return &PriceService{
		priceRepository: priceRepository,
		tokenProvider:   tokenProvider,
		notionalCache:   notionalCache,
		logger:          logger.With(zap.String("module", "priceService")),
	}
}

// GetPrice returns the price of a token at a given datetime.
func (s *PriceService) GetPrice(ctx context.Context, tokenChainID sdk.ChainID, tokenAddress string, datetime time.Time) (*Price, error) {
	log := s.logger.With(zap.Uint16("chainID", uint16(tokenChainID)), zap.String("tokenAddress", tokenAddress))
	token, found := s.tokenProvider.GetTokenByAddress(tokenChainID, tokenAddress)
	if !found {
		log.Warn("Token not found")
		return nil, ErrTokenNotFound
	}

	v, err := s.GetPriceBySymbol(ctx, token, datetime, log)
	if err != nil {
		return nil, err
	}

	return &Price{
		CoingeckoID: token.CoingeckoID,
		Symbol:      token.Symbol.String(),
		Price:       v.price.String(),
		Datetime:    v.datetime,
	}, nil
}

func (s *PriceService) GetPriceByCoingeckoID(ctx context.Context, coingeckoID string, datetime time.Time) (*Price, error) {
	log := s.logger.With(zap.String("coingeckoId", coingeckoID))
	token, found := s.tokenProvider.GetTokenByCoingeckoID(coingeckoID)
	if !found {
		log.Warn("Token not found")
		return nil, ErrTokenNotFound
	}
	v, err := s.GetPriceBySymbol(ctx, token, datetime, log)
	if err != nil {
		return nil, err
	}

	return &Price{
		CoingeckoID: coingeckoID,
		Symbol:      token.Symbol.String(),
		Price:       v.price.String(),
		Datetime:    v.datetime,
	}, nil
}

type priceSymbol struct {
	price    decimal.Decimal
	datetime time.Time
}

func (s *PriceService) GetPriceBySymbol(ctx context.Context, token *domain.TokenMetadata, datetime time.Time, log *zap.Logger) (*priceSymbol, error) {
	if token.CoingeckoID == "" {
		log.Warn("CoingeckoID not found")
		return nil, ErrTokenNotFound
	}

	dayDatetime := datetime.Truncate(24 * time.Hour)
	cachePrice, err := s.notionalCache.Get(token.GetTokenID())
	if err != nil {
		return nil, err
	}

	diffCachePrice := cachePrice.UpdatedAt.Sub(datetime).Abs()
	diffDayPrice := dayDatetime.Sub(datetime).Abs()
	var price decimal.Decimal
	var priceDatetime time.Time
	if diffCachePrice > diffDayPrice {
		p, err := s.priceRepository.Find(ctx, token.CoingeckoID, dayDatetime)
		if err != nil {
			if err == ErrPriceNotFound {
				return nil, ErrTokenNotFound
			}
			log.Error("Failed to find price", zap.Error(err))
			return nil, err
		}
		price, err = decimal.NewFromString(p.Price)
		if err != nil {
			log.Error("Failed to parse price", zap.Error(err))
			return nil, err
		}
		priceDatetime = p.Datetime
	} else {
		price = cachePrice.NotionalUsd
		priceDatetime = cachePrice.UpdatedAt
	}
	return &priceSymbol{price: price, datetime: priceDatetime}, nil
}
