package currency

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/thrasher-corp/gocryptotrader/common/file"
	"github.com/thrasher-corp/gocryptotrader/currency/coinmarketcap"
	"github.com/thrasher-corp/gocryptotrader/currency/forexprovider"
	"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
	"github.com/thrasher-corp/gocryptotrader/log"
)

// storage is an overarching type that keeps track of and updates currency,
// currency exchange rates and pairs
var storage Storage

func init() {
	storage.SetDefaults()
}

// CurrencyFileUpdateDelay defines the rate at which the currency.json file is
// updated
const (
	DefaultCurrencyFileDelay             = 168 * time.Hour
	DefaultForeignExchangeDelay          = 1 * time.Minute
	DefaultStorageFile                   = "currency.json"
	DefaultForexProviderExchangeRatesAPI = "ExchangeRateHost"
)

var (
	// ErrFiatDisplayCurrencyIsNotFiat defines an error for when the fiat
	// display currency is not set as a fiat currency.
	ErrFiatDisplayCurrencyIsNotFiat = errors.New("fiat display currency is not a fiat currency")

	errUnexpectedRole                       = errors.New("unexpected currency role")
	errFiatDisplayCurrencyUnset             = errors.New("fiat display currency is unset")
	errNoFilePathSet                        = errors.New("no file path set")
	errInvalidCurrencyFileUpdateDuration    = errors.New("invalid currency file update duration")
	errInvalidForeignExchangeUpdateDuration = errors.New("invalid foreign exchange update duration")
	errNoForeignExchangeProvidersEnabled    = errors.New("no foreign exchange providers enabled")
	errNotFiatCurrency                      = errors.New("not a fiat currency")
	errInvalidAmount                        = errors.New("invalid amount")
)

// SetDefaults sets storage defaults for basic package functionality
func (s *Storage) SetDefaults() {
	s.defaultBaseCurrency = USD
	s.baseCurrency = s.defaultBaseCurrency
	fiatCurrencies := make([]Code, 0, len(symbols))
	for item := range symbols {
		if item == USDT.Item {
			continue
		}
		fiatCurrencies = append(fiatCurrencies, Code{Item: item, UpperCase: true})
	}

	err := s.SetDefaultFiatCurrencies(fiatCurrencies)
	if err != nil {
		log.Errorf(log.Currency, "Currency Storage: Setting default fiat currencies error: %s", err)
	}

	err = s.SetStableCoins(stables)
	if err != nil {
		log.Errorf(log.Currency, "Currency Storage: Setting default stable currencies error: %s", err)
	}

	err = s.SetDefaultCryptocurrencies(Currencies{BTC, LTC, ETH, DOGE, DASH, XRP, XMR, USDT, UST})
	if err != nil {
		log.Errorf(log.Currency, "Currency Storage: Setting default cryptocurrencies error: %s", err)
	}
	s.SetupConversionRates()
	s.fiatExchangeMarkets = forexprovider.NewDefaultFXProvider()
}

// RunUpdater runs the foreign exchange updater service. This will set up a JSON
// dump file and keep foreign exchange rates updated as fast as possible without
// triggering rate limiters, it will also run a full cryptocurrency check
// through coin market cap and expose analytics for exchange services
func (s *Storage) RunUpdater(overrides BotOverrides, settings *Config, filePath string) error {
	if settings.FiatDisplayCurrency.IsEmpty() {
		return errFiatDisplayCurrencyUnset
	}

	if !settings.FiatDisplayCurrency.IsFiatCurrency() {
		return fmt.Errorf("%s: %w", settings.FiatDisplayCurrency, ErrFiatDisplayCurrencyIsNotFiat)
	}

	if filePath == "" {
		return errNoFilePathSet
	}

	if settings.CurrencyFileUpdateDuration <= 0 {
		return errInvalidCurrencyFileUpdateDuration
	}

	if settings.ForeignExchangeUpdateDuration <= 0 {
		return errInvalidForeignExchangeUpdateDuration
	}

	s.mtx.Lock()
	s.shutdown = make(chan struct{})
	s.baseCurrency = settings.FiatDisplayCurrency
	s.path = filepath.Join(filePath, DefaultStorageFile)
	s.currencyFileUpdateDelay = settings.CurrencyFileUpdateDuration
	s.foreignExchangeUpdateDelay = settings.ForeignExchangeUpdateDuration

	log.Debugf(log.Currency, "Fiat display currency: %s.\n", s.baseCurrency)
	var err error
	if overrides.Coinmarketcap {
		if settings.CryptocurrencyProvider.APIKey != "" &&
			settings.CryptocurrencyProvider.APIKey != "Key" {
			log.Debugln(log.Currency, "Setting up currency analysis system with Coinmarketcap...")
			s.currencyAnalysis, err = coinmarketcap.NewFromSettings(coinmarketcap.Settings(settings.CryptocurrencyProvider))
			if err != nil {
				log.Errorf(log.Currency, "Unable to setup CoinMarketCap analysis. Error: %s", err)
			}
		} else {
			log.Warnf(log.Currency, "%s API key not set, disabling. Please set this in your config.json file\n",
				settings.CryptocurrencyProvider.Name)
		}
	}

	fxSettings := make([]base.Settings, 0, len(settings.ForexProviders))
	var primaryProvider bool
	for i := range settings.ForexProviders {
		enabled := (settings.ForexProviders[i].Name == "CurrencyConverter" && overrides.CurrencyConverter) ||
			(settings.ForexProviders[i].Name == "CurrencyLayer" && overrides.CurrencyLayer) ||
			(settings.ForexProviders[i].Name == "Fixer" && overrides.Fixer) ||
			(settings.ForexProviders[i].Name == "OpenExchangeRates" && overrides.OpenExchangeRates) ||
			(settings.ForexProviders[i].Name == "ExchangeRates" && overrides.ExchangeRates) ||
			(settings.ForexProviders[i].Name == "ExchangeRateHost" && overrides.ExchangeRateHost)

		if !enabled {
			continue
		}

		if settings.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI {
			if settings.ForexProviders[i].APIKey == "" || settings.ForexProviders[i].APIKey == "Key" {
				log.Warnf(log.Currency, "%s forex provider API key not set, disabling. Please set this in your config.json file\n",
					settings.ForexProviders[i].Name)
				settings.ForexProviders[i].Enabled = false
				settings.ForexProviders[i].PrimaryProvider = false
				continue
			}

			if settings.ForexProviders[i].APIKeyLvl == -1 && settings.ForexProviders[i].Name != "ExchangeRates" {
				log.Warnf(log.Currency, "%s APIKey level not set, functionality is limited. Please review this in your config.json file\n",
					settings.ForexProviders[i].Name)
			}
		}

		if settings.ForexProviders[i].PrimaryProvider {
			if primaryProvider {
				log.Warnf(log.Currency, "%s disabling primary provider, multiple primarys found. Please review providers in your config.json file\n",
					settings.ForexProviders[i].Name)
				settings.ForexProviders[i].PrimaryProvider = false
			} else {
				primaryProvider = true
			}
		}
		fxSettings = append(fxSettings, base.Settings(settings.ForexProviders[i]))
	}

	if len(fxSettings) == 0 {
		log.Warnln(log.Currency, "No foreign exchange providers enabled, setting default provider...")
		for x := range settings.ForexProviders {
			if settings.ForexProviders[x].Name != DefaultForexProviderExchangeRatesAPI {
				continue
			}
			settings.ForexProviders[x].Enabled = true
			settings.ForexProviders[x].PrimaryProvider = true
			primaryProvider = true
			log.Warnf(log.Currency, "No valid foreign exchange providers configured. Defaulting to %s.", DefaultForexProviderExchangeRatesAPI)
			fxSettings = append(fxSettings, base.Settings(settings.ForexProviders[x]))
		}
	}

	if len(fxSettings) == 0 {
		s.mtx.Unlock()
		return errNoForeignExchangeProvidersEnabled
	}

	if !primaryProvider {
		for x := range settings.ForexProviders {
			if settings.ForexProviders[x].Name == fxSettings[0].Name {
				settings.ForexProviders[x].PrimaryProvider = true
				fxSettings[0].PrimaryProvider = true
				log.Warnf(log.Currency, "No primary foreign exchange provider set. Defaulting to %s.", fxSettings[0].Name)
				break
			}
		}
	}

	s.fiatExchangeMarkets, err = forexprovider.StartFXService(fxSettings)
	if err != nil {
		s.mtx.Unlock()
		return err
	}

	log.Debugf(log.Currency, "Using primary foreign exchange provider %s\n",
		s.fiatExchangeMarkets.Primary.Provider.GetName())

	for i := range s.fiatExchangeMarkets.Support {
		log.Debugf(log.Currency, "Supporting foreign exchange provider %s\n",
			s.fiatExchangeMarkets.Support[i].Provider.GetName())
	}

	// Mutex present in this go routine to lock down retrieving rate data
	// until this system initially updates
	s.wg.Add(1)
	go s.ForeignExchangeUpdater()
	return nil
}

// SetupConversionRates sets default conversion rate values
func (s *Storage) SetupConversionRates() {
	s.fxRates = ConversionRates{
		m: make(map[*Item]map[*Item]*float64),
	}
}

// SetDefaultFiatCurrencies assigns the default fiat currency list and adds it
// to the running list
func (s *Storage) SetDefaultFiatCurrencies(c Currencies) error {
	for i := range c {
		err := s.currencyCodes.UpdateCurrency(&Item{
			ID:         c[i].Item.ID,
			FullName:   c[i].Item.FullName,
			Symbol:     c[i].Item.Symbol,
			Lower:      c[i].Item.Lower,
			Role:       Fiat,
			AssocChain: c[i].Item.AssocChain,
		})
		if err != nil {
			return err
		}
	}
	s.defaultFiatCurrencies = append(s.defaultFiatCurrencies, c...)
	s.fiatCurrencies = append(s.fiatCurrencies, c...)
	return nil
}

// SetStableCoins assigns the stable currency list and adds it to the running
// list
func (s *Storage) SetStableCoins(c Currencies) error {
	for i := range c {
		err := s.currencyCodes.UpdateCurrency(&Item{
			ID:         c[i].Item.ID,
			FullName:   c[i].Item.FullName,
			Symbol:     c[i].Item.Symbol,
			Lower:      c[i].Item.Lower,
			Role:       Stable,
			AssocChain: c[i].Item.AssocChain,
		})
		if err != nil {
			return err
		}
	}
	s.stableCurrencies = append(s.stableCurrencies, c...)
	return nil
}

// SetDefaultCryptocurrencies assigns the default cryptocurrency list and adds
// it to the running list
func (s *Storage) SetDefaultCryptocurrencies(c Currencies) error {
	for i := range c {
		err := s.currencyCodes.UpdateCurrency(&Item{
			ID:         c[i].Item.ID,
			FullName:   c[i].Item.FullName,
			Symbol:     c[i].Item.Symbol,
			Lower:      c[i].Item.Lower,
			Role:       Cryptocurrency,
			AssocChain: c[i].Item.AssocChain,
		})
		if err != nil {
			return err
		}
	}
	s.defaultCryptoCurrencies = append(s.defaultCryptoCurrencies, c...)
	s.cryptocurrencies = append(s.cryptocurrencies, c...)
	return nil
}

// SetupForexProviders sets up a new instance of the forex providers
func (s *Storage) SetupForexProviders(setting ...base.Settings) error {
	addr, err := forexprovider.StartFXService(setting)
	if err != nil {
		return err
	}

	s.fiatExchangeMarkets = addr
	return nil
}

// ForeignExchangeUpdater is a routine that seeds foreign exchange rate and keeps
// updated as fast as possible
func (s *Storage) ForeignExchangeUpdater() {
	defer s.wg.Done()
	log.Debugln(log.Currency, "Foreign exchange updater started, seeding FX rate list...")

	err := s.SeedCurrencyAnalysisData()
	if err != nil {
		log.Errorln(log.Currency, err)
	}

	err = s.SeedForeignExchangeRates()
	if err != nil {
		log.Errorln(log.Currency, err)
	}

	// Unlock main rate retrieval mutex so all routines waiting can get access
	// to data
	s.mtx.Unlock()

	// Set tickers to client defined rates or defaults
	SeedForeignExchangeTick := time.NewTicker(s.foreignExchangeUpdateDelay)
	SeedCurrencyAnalysisTick := time.NewTicker(s.currencyFileUpdateDelay)
	defer SeedForeignExchangeTick.Stop()
	defer SeedCurrencyAnalysisTick.Stop()

	for {
		select {
		case <-s.shutdown:
			return
		case <-SeedForeignExchangeTick.C:
			go func() {
				err := s.SeedForeignExchangeRates()
				if err != nil {
					log.Errorln(log.Currency, err)
				}
			}()
		case <-SeedCurrencyAnalysisTick.C:
			go func() {
				err := s.SeedCurrencyAnalysisData()
				if err != nil {
					log.Errorln(log.Currency, err)
				}
			}()
		}
	}
}

// SeedCurrencyAnalysisData sets a new instance of a coinmarketcap data.
func (s *Storage) SeedCurrencyAnalysisData() error {
	if s.currencyCodes.LastMainUpdate.IsZero() {
		b, err := os.ReadFile(s.path)
		if err != nil {
			return s.FetchCurrencyAnalysisData()
		}
		var f *File
		err = json.Unmarshal(b, &f)
		if err != nil {
			return err
		}
		err = s.LoadFileCurrencyData(f)
		if err != nil {
			return err
		}
	}

	// Based on update delay update the file
	if time.Now().After(s.currencyCodes.LastMainUpdate.Add(s.currencyFileUpdateDelay)) ||
		s.currencyCodes.LastMainUpdate.IsZero() {
		err := s.FetchCurrencyAnalysisData()
		if err != nil {
			return err
		}
	}

	return nil
}

// FetchCurrencyAnalysisData fetches a new fresh batch of currency data and
// loads it into memory
func (s *Storage) FetchCurrencyAnalysisData() error {
	if s.currencyAnalysis == nil {
		log.Warnln(log.Currency,
			"Currency analysis system offline, please set api keys for coinmarketcap if you wish to use this feature.")
		return errors.New("currency analysis system offline")
	}

	return s.UpdateCurrencies()
}

// WriteCurrencyDataToFile writes the full currency data to a designated file
func (s *Storage) WriteCurrencyDataToFile(path string, mainUpdate bool) error {
	data, err := s.currencyCodes.GetFullCurrencyData()
	if err != nil {
		return err
	}

	if mainUpdate {
		t := time.Now()
		data.LastMainUpdate = t.Unix()
		s.currencyCodes.LastMainUpdate = t
	}

	var encoded []byte
	encoded, err = json.MarshalIndent(data, "", " ")
	if err != nil {
		return err
	}

	return file.Write(path, encoded)
}

func (s *Storage) checkFileCurrencyData(item *Item, role Role) error {
	if item.Role == Unset {
		item.Role = role
	}
	if item.Role != role {
		return fmt.Errorf("%w %s expecting: %s", errUnexpectedRole, item.Role, role)
	}
	return s.currencyCodes.LoadItem(item)
}

// LoadFileCurrencyData loads currencies into the currency codes
func (s *Storage) LoadFileCurrencyData(f *File) error {
	for i := range f.Contracts {
		err := s.checkFileCurrencyData(f.Contracts[i], Contract)
		if err != nil {
			return err
		}
	}

	for i := range f.Cryptocurrency {
		err := s.checkFileCurrencyData(f.Cryptocurrency[i], Cryptocurrency)
		if err != nil {
			return err
		}
	}

	for i := range f.Token {
		err := s.checkFileCurrencyData(f.Token[i], Token)
		if err != nil {
			return err
		}
	}

	for i := range f.FiatCurrency {
		err := s.checkFileCurrencyData(f.FiatCurrency[i], Fiat)
		if err != nil {
			return err
		}
	}

	for i := range f.UnsetCurrency {
		err := s.checkFileCurrencyData(f.UnsetCurrency[i], Unset)
		if err != nil {
			return err
		}
	}

	for i := range f.Stable {
		err := s.checkFileCurrencyData(f.Stable[i], Stable)
		if err != nil {
			return err
		}
	}

	switch t := f.LastMainUpdate.(type) {
	case string:
		parseT, err := time.Parse(time.RFC3339Nano, t)
		if err != nil {
			return err
		}
		s.currencyCodes.LastMainUpdate = parseT
	case float64:
		s.currencyCodes.LastMainUpdate = time.Unix(int64(t), 0)
	default:
		return errors.New("unhandled type conversion for LastMainUpdate time")
	}

	return nil
}

// UpdateCurrencies updates currency role and information using coin market cap
func (s *Storage) UpdateCurrencies() error {
	currencyUpdates, err := s.currencyAnalysis.GetCryptocurrencyIDMap()
	if err != nil {
		return err
	}

	for x := range currencyUpdates {
		if currencyUpdates[x].IsActive != 1 {
			continue
		}

		update := &Item{
			FullName:   currencyUpdates[x].Name,
			Symbol:     currencyUpdates[x].Symbol,
			AssocChain: currencyUpdates[x].Platform.Symbol,
			ID:         currencyUpdates[x].ID,
			Role:       Cryptocurrency,
		}

		if currencyUpdates[x].Platform.Symbol != "" {
			update.Role = Token
		}

		err = s.currencyCodes.UpdateCurrency(update)
		if err != nil {
			return err
		}
	}
	return nil
}

// SeedForeignExchangeRatesByCurrencies seeds the foreign exchange rates by
// currencies supplied
func (s *Storage) SeedForeignExchangeRatesByCurrencies(c Currencies) error {
	s.fxRates.mtx.Lock()
	defer s.fxRates.mtx.Unlock()
	rates, err := s.fiatExchangeMarkets.GetCurrencyData(s.baseCurrency.String(),
		c.Strings())
	if err != nil {
		return err
	}
	return s.updateExchangeRates(rates)
}

// SeedForeignExchangeRate returns a singular exchange rate
func (s *Storage) SeedForeignExchangeRate(from, to Code) (map[string]float64, error) {
	return s.fiatExchangeMarkets.GetCurrencyData(from.String(),
		[]string{to.String()})
}

// GetDefaultForeignExchangeRates returns foreign exchange rates based off
// default fiat currencies.
func (s *Storage) GetDefaultForeignExchangeRates() (Conversions, error) {
	if !s.updaterRunning {
		err := s.SeedDefaultForeignExchangeRates()
		if err != nil {
			return nil, err
		}
	}
	return s.fxRates.GetFullRates(), nil
}

// SeedDefaultForeignExchangeRates seeds the default foreign exchange rates
func (s *Storage) SeedDefaultForeignExchangeRates() error {
	s.fxRates.mtx.Lock()
	defer s.fxRates.mtx.Unlock()
	rates, err := s.fiatExchangeMarkets.GetCurrencyData(
		s.defaultBaseCurrency.String(),
		s.defaultFiatCurrencies.Strings())
	if err != nil {
		return err
	}
	return s.updateExchangeRates(rates)
}

// GetExchangeRates returns storage seeded exchange rates
func (s *Storage) GetExchangeRates() (Conversions, error) {
	if !s.updaterRunning {
		err := s.SeedForeignExchangeRates()
		if err != nil {
			return nil, err
		}
	}
	return s.fxRates.GetFullRates(), nil
}

// SeedForeignExchangeRates seeds the foreign exchange rates from storage config
// currencies
func (s *Storage) SeedForeignExchangeRates() error {
	s.fxRates.mtx.Lock()
	defer s.fxRates.mtx.Unlock()
	rates, err := s.fiatExchangeMarkets.GetCurrencyData(s.baseCurrency.String(),
		s.fiatCurrencies.Strings())
	if err != nil {
		return err
	}
	return s.updateExchangeRates(rates)
}

// UpdateForeignExchangeRates sets exchange rates on the FX map
func (s *Storage) updateExchangeRates(m map[string]float64) error {
	return s.fxRates.Update(m)
}

// GetTotalMarketCryptocurrencies returns the total seeded market
// cryptocurrencies
func (s *Storage) GetTotalMarketCryptocurrencies() (Currencies, error) {
	if !s.currencyCodes.HasData() {
		return nil, errors.New("market currency codes not populated")
	}
	return s.currencyCodes.GetCurrencies(), nil
}

// IsDefaultCurrency returns if a currency is a default currency
func (s *Storage) IsDefaultCurrency(c Code) bool {
	for i := range s.defaultFiatCurrencies {
		if s.defaultFiatCurrencies[i].Equal(c) ||
			s.defaultFiatCurrencies[i].Equal(GetTranslation(c)) {
			return true
		}
	}
	return false
}

// IsDefaultCryptocurrency returns if a cryptocurrency is a default
// cryptocurrency
func (s *Storage) IsDefaultCryptocurrency(c Code) bool {
	for i := range s.defaultCryptoCurrencies {
		if s.defaultCryptoCurrencies[i].Equal(c) ||
			s.defaultCryptoCurrencies[i].Equal(GetTranslation(c)) {
			return true
		}
	}
	return false
}

// ValidateCode validates string against currency list and returns a currency
// code
func (s *Storage) ValidateCode(newCode string) Code {
	return s.currencyCodes.Register(newCode, Unset)
}

// ValidateFiatCode validates a fiat currency string and returns a currency
// code
func (s *Storage) ValidateFiatCode(newCode string) Code {
	c := s.currencyCodes.Register(newCode, Fiat)
	if !s.fiatCurrencies.Contains(c) {
		s.fiatCurrencies = append(s.fiatCurrencies, c)
	}
	return c
}

// UpdateBaseCurrency changes base currency
func (s *Storage) UpdateBaseCurrency(c Code) error {
	if c.IsFiatCurrency() {
		s.baseCurrency = c
		return nil
	}
	return fmt.Errorf("currency %s not fiat failed to set currency", c)
}

// GetCryptocurrencies returns the cryptocurrency list
func (s *Storage) GetCryptocurrencies() Currencies {
	return s.cryptocurrencies
}

// GetDefaultCryptocurrencies returns a list of default cryptocurrencies
func (s *Storage) GetDefaultCryptocurrencies() Currencies {
	return s.defaultCryptoCurrencies
}

// GetFiatCurrencies returns the fiat currencies list
func (s *Storage) GetFiatCurrencies() Currencies {
	return s.fiatCurrencies
}

// GetDefaultFiatCurrencies returns the default fiat currencies list
func (s *Storage) GetDefaultFiatCurrencies() Currencies {
	return s.defaultFiatCurrencies
}

// GetDefaultBaseCurrency returns the default base currency
func (s *Storage) GetDefaultBaseCurrency() Code {
	return s.defaultBaseCurrency
}

// GetBaseCurrency returns the current storage base currency
func (s *Storage) GetBaseCurrency() Code {
	return s.baseCurrency
}

// UpdateEnabledCryptoCurrencies appends new cryptocurrencies to the enabled
// currency list
func (s *Storage) UpdateEnabledCryptoCurrencies(c Currencies) {
	for i := range c {
		if !s.cryptocurrencies.Contains(c[i]) {
			s.cryptocurrencies = append(s.cryptocurrencies, c[i])
		}
	}
}

// UpdateEnabledFiatCurrencies appends new fiat currencies to the enabled
// currency list
func (s *Storage) UpdateEnabledFiatCurrencies(c Currencies) {
	for i := range c {
		if !s.fiatCurrencies.Contains(c[i]) &&
			!s.cryptocurrencies.Contains(c[i]) {
			s.fiatCurrencies = append(s.fiatCurrencies, c[i])
		}
	}
}

// ConvertCurrency for example converts $1 USD to the equivalent Japanese Yen
// or vice versa.
func (s *Storage) ConvertCurrency(amount float64, from, to Code) (float64, error) {
	if amount <= 0 {
		return 0, fmt.Errorf("%f %w", amount, errInvalidAmount)
	}
	if !from.IsFiatCurrency() {
		return 0, fmt.Errorf("%s %w", from, errNotFiatCurrency)
	}
	if !to.IsFiatCurrency() {
		return 0, fmt.Errorf("%s %w", to, errNotFiatCurrency)
	}

	if from.Equal(to) { // No need to lock down storage for this rate.
		return amount, nil
	}

	s.mtx.Lock()
	defer s.mtx.Unlock()
	if !s.fxRates.HasData() {
		err := s.SeedDefaultForeignExchangeRates()
		if err != nil {
			return 0, err
		}
	}

	r, err := s.fxRates.GetRate(from, to)
	if err != nil {
		return 0, err
	}

	return r * amount, nil
}

// GetStorageRate returns the rate of the conversion value
func (s *Storage) GetStorageRate(from, to Code) (float64, error) {
	s.mtx.Lock()
	defer s.mtx.Unlock()

	if !s.fxRates.HasData() {
		err := s.SeedDefaultForeignExchangeRates()
		if err != nil {
			return 0, err
		}
	}

	return s.fxRates.GetRate(from, to)
}

// NewConversion returns a new conversion object that has a pointer to a related
// rate with its inversion.
func (s *Storage) NewConversion(from, to Code) (Conversion, error) {
	s.mtx.Lock()
	defer s.mtx.Unlock()

	if !s.fxRates.HasData() {
		err := storage.SeedDefaultForeignExchangeRates()
		if err != nil {
			return Conversion{}, err
		}
	}
	return s.fxRates.Register(from, to)
}

// IsVerbose returns if the storage is in verbose mode
func (s *Storage) IsVerbose() bool {
	return s.Verbose
}

// Shutdown shuts down the currency storage system and saves to currency.json
func (s *Storage) Shutdown() error {
	s.mtx.Lock()
	defer s.mtx.Unlock()
	if s.shutdown == nil {
		return nil
	}
	close(s.shutdown)
	s.wg.Wait()
	s.shutdown = nil
	return s.WriteCurrencyDataToFile(s.path, true)
}
