package coinmarketcap

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"sort"
	"strings"
	"time"

	apitypes "github.com/cointop-sh/cointop/pkg/api/types"
	"github.com/cointop-sh/cointop/pkg/api/util"
	cmc "github.com/miguelmota/go-coinmarketcap/pro/v1"
	cmcv2 "github.com/miguelmota/go-coinmarketcap/v2"
)

// ErrQuoteNotFound is the error for when a quote is not found
var ErrQuoteNotFound = errors.New("quote not found")

// ErrPingFailed is the error for when pinging the API fails
var ErrPingFailed = errors.New("failed to ping")

// ErrFetchGraphData is the error for when fetching graph data fails
var ErrFetchGraphData = errors.New("graph data fetch error")

// Service service
type Service struct {
	client *cmc.Client
}

// NewCMC new service
func NewCMC(apiKey string) *Service {
	if apiKey == "" {
		apiKey = os.Getenv("CMC_PRO_API_KEY")
	}
	client := cmc.NewClient(&cmc.Config{
		ProAPIKey: apiKey,
	})
	return &Service{
		client: client,
	}
}

// Ping ping API
func (s *Service) Ping() error {
	info, err := s.client.Cryptocurrency.Info(&cmc.InfoOptions{
		Symbol: "BTC",
	})
	if err != nil {
		return ErrPingFailed
	}
	if info == nil {
		return ErrPingFailed
	}
	return nil
}

func (s *Service) getPaginatedCoinData(convert string, offset int) ([]apitypes.Coin, error) {
	var ret []apitypes.Coin
	max := 100

	listings, err := s.client.Cryptocurrency.LatestListings(&cmc.ListingOptions{
		Limit:   max,
		Convert: convert,
		Start:   max * offset,
	})
	if err != nil {
		return nil, err
	}
	for _, v := range listings {
		quote, ok := v.Quote[convert]
		if !ok {
			return nil, ErrQuoteNotFound
		}

		ret = append(ret, apitypes.Coin{
			ID:               util.FormatID(v.Name),
			Name:             util.FormatName(v.Name),
			Symbol:           util.FormatSymbol(v.Symbol),
			Rank:             util.FormatRank(v.CMCRank),
			AvailableSupply:  util.FormatSupply(v.CirculatingSupply),
			TotalSupply:      util.FormatSupply(v.TotalSupply),
			MarketCap:        util.FormatMarketCap(quote.MarketCap),
			Price:            util.FormatPrice(v.Quote[convert].Price, convert),
			PercentChange1H:  util.FormatPercentChange(quote.PercentChange1H),
			PercentChange24H: util.FormatPercentChange(quote.PercentChange24H),
			PercentChange7D:  util.FormatPercentChange(quote.PercentChange7D),
			Volume24H:        util.FormatVolume(v.Quote[convert].Volume24H),
			LastUpdated:      util.FormatLastUpdated(v.LastUpdated),
		})
	}
	return ret, nil
}

// GetAllCoinData gets all coin data. Need to paginate through all pages
func (s *Service) GetAllCoinData(convert string, ch chan []apitypes.Coin) error {
	go func() {
		maxPages := 10
		defer close(ch)
		for i := 0; i < maxPages; i++ {
			if i > 0 {
				time.Sleep(1 * time.Second)
			}

			coins, err := s.getPaginatedCoinData(convert, i)
			if err != nil {
				return
			}

			ch <- coins
		}
	}()
	return nil
}

// GetCoinData gets all data of a coin.
func (s *Service) GetCoinData(name string, convert string) (apitypes.Coin, error) {
	ret := apitypes.Coin{}
	coins, err := s.getPaginatedCoinData(convert, 0)
	if err != nil {
		return ret, err
	}

	for _, coin := range coins {
		if coin.Name == name {
			return coin, nil
		}
	}

	return ret, nil
}

// GetCoinDataBatch gets all data of specified coins.
func (s *Service) GetCoinDataBatch(names []string, convert string) ([]apitypes.Coin, error) {
	var ret []apitypes.Coin
	coins, err := s.getPaginatedCoinData(convert, 0)
	if err != nil {
		return ret, err
	}

	for _, coin := range coins {
		for _, name := range names {
			if coin.Name == name {
				ret = append(ret, coin)
				break
			}
		}
	}

	return ret, nil
}

// GetCoinGraphData gets coin graph data
func (s *Service) GetCoinGraphData(convert, symbol string, name string, start int64, end int64) (apitypes.CoinGraph, error) {
	ret := apitypes.CoinGraph{}
	symbol = strings.ToUpper(symbol)
	info, err := s.client.Cryptocurrency.Info(&cmc.InfoOptions{
		Slug: name,
	})
	if err != nil {
		return ret, err
	}
	var coinID string
	if len(info) == 0 {
		return ret, ErrFetchGraphData
	}
	for k := range info {
		coinID = fmt.Sprintf("%v", info[k].ID)
	}
	if convert == "" {
		convert = "usd"
	}
	convert = strings.ToUpper(convert)
	interval := getChartInterval(start, end)
	params := []string{
		fmt.Sprintf("convert=%s,%s", convert, symbol),
		"format=chart_crypto_details",
		fmt.Sprintf("id=%s", coinID),
		fmt.Sprintf("interval=%s", interval),
		fmt.Sprintf("time_start=%v", start),
		fmt.Sprintf("time_end=%v", end),
	}
	baseURL := "https://web-api.coinmarketcap.com/v1.1"
	url := fmt.Sprintf("%s/cryptocurrency/quotes/historical?%s", baseURL, strings.Join(params, "&"))
	resp, err := makeReq(url)
	if err != nil {
		return ret, err
	}
	var result map[string]interface{}
	err = json.Unmarshal(resp, &result)
	if err != nil {
		return ret, err
	}
	data, ok := result["data"]
	if !ok {
		return ret, ErrFetchGraphData
	}
	ifcs, ok := data.(map[string]interface{})
	if !ok {
		return ret, ErrFetchGraphData
	}
	var prices [][]float64
	for datetime, item := range ifcs {
		ifc, ok := item.(map[string]interface{})
		if !ok {
			return ret, ErrFetchGraphData
		}
		for key, obj := range ifc {
			if key != convert {
				continue
			}
			arrIfc, ok := obj.([]interface{})
			if !ok {
				return ret, ErrFetchGraphData
			}
			if len(arrIfc) == 0 {
				return ret, ErrFetchGraphData
			}
			val := arrIfc[0].(float64)
			t, err := time.Parse(time.RFC3339, datetime)
			if err != nil {
				return ret, err
			}
			prices = append(prices, []float64{float64(t.Unix()), val})
		}
	}
	sort.Slice(prices[:], func(i, j int) bool {
		return prices[i][0] < prices[j][0]
	})
	ret.Price = prices
	return ret, nil
}

// GetGlobalMarketGraphData gets global market graph data
func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int64) (apitypes.MarketGraph, error) {
	ret := apitypes.MarketGraph{}
	if convert == "" {
		convert = "usd"
	}
	convert = strings.ToUpper(convert)
	interval := getChartInterval(start, end)
	params := []string{
		fmt.Sprintf("convert=%s", convert),
		"format=chart",
		fmt.Sprintf("interval=%s", interval),
		fmt.Sprintf("time_start=%v", start),
		fmt.Sprintf("time_end=%v", end),
	}
	baseURL := "https://web-api.coinmarketcap.com/v1.1"
	url := fmt.Sprintf("%s/global-metrics/quotes/historical?%s", baseURL, strings.Join(params, "&"))
	resp, err := makeReq(url)
	if err != nil {
		return ret, err
	}
	var result map[string]interface{}
	err = json.Unmarshal(resp, &result)
	if err != nil {
		return ret, err
	}
	data, ok := result["data"]
	if !ok {
		return ret, ErrFetchGraphData
	}
	mapIfc, ok := data.(map[string]interface{})
	if !ok {
		return ret, ErrFetchGraphData
	}
	var marketCap [][]float64
	for datetime, item := range mapIfc {
		arrIfc, ok := item.([]interface{})
		if !ok {
			return ret, ErrFetchGraphData
		}
		if len(arrIfc) == 0 {
			return ret, ErrFetchGraphData
		}
		val := arrIfc[0].(float64)
		t, err := time.Parse(time.RFC3339, datetime)
		if err != nil {
			return ret, err
		}
		marketCap = append(marketCap, []float64{float64(t.Unix()), val})
	}
	sort.Slice(marketCap[:], func(i, j int) bool {
		return marketCap[i][0] < marketCap[j][0]
	})
	ret.MarketCapByAvailableSupply = marketCap
	return ret, nil
}

// GetGlobalMarketData gets global market data
func (s *Service) GetGlobalMarketData(convert string) (apitypes.GlobalMarketData, error) {
	ret := apitypes.GlobalMarketData{}
	market, err := s.client.GlobalMetrics.LatestQuotes(&cmc.QuoteOptions{
		Convert: convert,
	})

	if err != nil {
		return ret, err
	}
	ret = apitypes.GlobalMarketData{
		TotalMarketCapUSD:            market.Quote[convert].TotalMarketCap,
		Total24HVolumeUSD:            market.Quote[convert].TotalVolume24H,
		BitcoinPercentageOfMarketCap: market.BTCDominance,
		ActiveCurrencies:             int(market.ActiveCryptocurrencies),
		ActiveAssets:                 0,
		ActiveMarkets:                int(market.ActiveMarketPairs),
	}
	return ret, nil
}

// Price returns the current price of the coin
func (s *Service) Price(name string, convert string) (float64, error) {
	convert = strings.ToUpper(convert)
	symbol, err := cmcv2.CoinSymbol(util.NameToSlug(name))
	if err != nil {
		return 0, err
	}

	price, err := cmcv2.Price(&cmcv2.PriceOptions{
		Symbol:  symbol,
		Convert: convert,
	})
	if err != nil {
		return 0, err
	}

	return util.FormatPrice(price, convert), nil
}

// CoinLink returns the URL link for the coin
func (s *Service) CoinLink(name string) string {
	slug := util.NameToSlug(name)
	return fmt.Sprintf("https://coinmarketcap.com/currencies/%s", slug)
}

// SupportedCurrencies returns a list of supported currencies
func (s *Service) SupportedCurrencies() []string {
	// keep these in alphabetical order
	return []string{
		"AUD",
		"BRL",
		"BTC",
		"CAD",
		"CFH",
		"CLP",
		"CNY",
		"CZK",
		"DKK",
		"ETH",
		"EUR",
		"GBP",
		"HKD",
		"HUF",
		"IDR",
		"ILS",
		"INR",
		"JPY",
		"KRW",
		"MXN",
		"MYR",
		"NOK",
		"NZD",
		"PHP",
		"PKR",
		"PLN",
		"RUB",
		"SEK",
		"SGD",
		"THB",
		"TRY",
		"TWD",
		"USD",
		"VND",
		"ZAR",
	}
}

// doReq does HTTP request with client
func doReq(req *http.Request) ([]byte, error) {
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("%s", body)
	}

	return body, nil
}

// makeReq is an HTTP GET request helper
func makeReq(url string) ([]byte, error) {
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	resp, err := doReq(req)
	if err != nil {
		return nil, err
	}

	return resp, err
}

// getChartInterval returns the interval to use for given time range
func getChartInterval(start, end int64) string {
	interval := "15m"
	delta := end - start
	if delta >= 604800 {
		interval = "1h"
	}
	if delta >= 2629746 {
		interval = "1d"
	}
	if delta >= 604800 {
		interval = "1h"
	}
	if delta >= 2592000 {
		interval = "1d"
	}
	return interval
}

// GetExchangeRate gets the current excange rate between two currencies
func (s *Service) GetExchangeRate(convertFrom, convertTo string, cached bool) (float64, error) {
	if convertFrom == convertTo {
		return 1.0, nil
	}
	return 0, fmt.Errorf("unsupported currency conversion: %s => %s", convertFrom, convertTo)
}
