// Package coinmarketcap Coin Market Cap API client for Go
package coinmarketcap

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"

	"sort"

	"github.com/anaskhan96/soup"
	"github.com/miguelmota/go-coinmarketcap/v2/types"
)

var (
	siteURL               = "https://coinmarketcap.com"
	baseURL               = "https://api.coinmarketcap.com/v2"
	coinGraphURL          = "https://graphs2.coinmarketcap.com/currencies"
	globalMarketGraphURL  = "https://graphs2.coinmarketcap.com/global/marketcap-total"
	altcoinMarketGraphURL = "https://graphs2.coinmarketcap.com/global/marketcap-altcoin"
)

// Interface interface
type Interface interface {
	Listings() ([]*types.Listing, error)
	Tickers(options *TickersOptions) ([]*types.Ticker, error)
	Ticker(options *TickerOptions) (*types.Ticker, error)
	TickerGraph(options *TickerGraphOptions) (*types.TickerGraph, error)
	GlobalMarket(options *GlobalMarketOptions) (*types.GlobalMarket, error)
	GlobalMarketGraph(options *GlobalMarketGraphOptions) (*types.MarketGraph, error)
	GlobalAltcoinMarketGraph(options *GlobalAltcoinMarketGraphOptions) (*types.MarketGraph, error)
	Markets(options *MarketsOptions) ([]*types.Market, error)
	Price(options *PriceOptions) (float64, error)
	CoinID(symbol string) (int, error)
	CoinSlug(symbol string) (string, error)
	CoinSymbol(slug string) (string, error)
}

// listingsMedia listings response media
type listingsMedia struct {
	Data []*types.Listing `json:"data"`
}

// Listings gets all coin listings
func Listings() ([]*types.Listing, error) {
	url := fmt.Sprintf("%s/listings", baseURL)
	resp, err := makeReq(url)
	var body listingsMedia
	err = json.Unmarshal(resp, &body)
	if err != nil {
		return nil, err
	}
	return body.Data, nil
}

// TickersOptions options for tickers method
type TickersOptions struct {
	Start   int
	Limit   int
	Convert string
	Sort    string
}

// tickerMedia tickers response media
type tickersMedia struct {
	Data     map[string]*types.Ticker `json:"data,omitempty"`
	Metadata struct {
		Timestamp           int64
		NumCryptoCurrencies int    `json:"num_cryptocurrencies,omitempty"`
		Error               string `json:",omitempty"`
	}
}

// Tickers gets ticker information on coins
func Tickers(options *TickersOptions) ([]*types.Ticker, error) {
	var params []string
	if options.Start >= 0 {
		params = append(params, fmt.Sprintf("start=%v", options.Start))
	}
	if options.Limit >= 0 {
		params = append(params, fmt.Sprintf("limit=%v", options.Limit))
	}
	if options.Convert != "" {
		params = append(params, fmt.Sprintf("convert=%v", options.Convert))
	}
	if options.Sort != "" {
		params = append(params, fmt.Sprintf("sort=%v", options.Sort))
	}
	url := fmt.Sprintf("%s/ticker?%s", baseURL, strings.Join(params, "&"))
	resp, err := makeReq(url)
	var body tickersMedia
	err = json.Unmarshal(resp, &body)
	if err != nil {
		return nil, err
	}
	data := body.Data
	var tickers []*types.Ticker
	for _, v := range data {
		tickers = append(tickers, v)
	}

	if body.Metadata.Error != "" {
		return nil, errors.New(body.Metadata.Error)
	}

	sort.Slice(tickers, func(i, j int) bool {
		return tickers[i].Rank < tickers[j].Rank
	})

	return tickers, nil
}

// TickerOptions options for ticker method
type TickerOptions struct {
	Symbol  string
	Convert string
}

type tickerMedia struct {
	Data *types.Ticker `json:"data"`
}

// Ticker gets ticker information about a cryptocurrency
func Ticker(options *TickerOptions) (*types.Ticker, error) {
	var params []string
	if options.Convert != "" {
		params = append(params, fmt.Sprintf("convert=%v", options.Convert))
	}
	id, err := CoinID(options.Symbol)
	if err != nil {
		return nil, err
	}
	url := fmt.Sprintf("%s/ticker/%v?%s", baseURL, id, strings.Join(params, "&"))
	resp, err := makeReq(url)
	if err != nil {
		return nil, err
	}
	var body tickerMedia
	err = json.Unmarshal(resp, &body)
	if err != nil {
		return nil, err
	}
	return body.Data, nil
}

// TickerGraphOptions options for ticker graph
type TickerGraphOptions struct {
	Symbol string
	Start  int64
	End    int64
}

// TickerGraph gets graph data points for a cryptocurrency
func TickerGraph(options *TickerGraphOptions) (*types.TickerGraph, error) {
	slug, err := CoinSlug(options.Symbol)
	if err != nil {
		return nil, err
	}
	url := fmt.Sprintf("%s/%s/%d/%d", coinGraphURL, slug, options.Start*1000, options.End*1000)
	resp, err := makeReq(url)
	if err != nil {
		return nil, err
	}
	var data *types.TickerGraph
	err = json.Unmarshal(resp, &data)
	if err != nil {
		return nil, err
	}
	return data, nil
}

// GlobalMarketOptions options for global data method
type GlobalMarketOptions struct {
	Convert string
}

// globalMedia global data response media
type globalMarketMedia struct {
	Data *types.GlobalMarket `json:"data"`
}

// GlobalMarket gets information about the global market of the cryptocurrencies
func GlobalMarket(options *GlobalMarketOptions) (*types.GlobalMarket, error) {
	var params []string
	if options.Convert != "" {
		params = append(params, fmt.Sprintf("convert=%v", options.Convert))
	}
	url := fmt.Sprintf("%s/global?%s", baseURL, strings.Join(params, "&"))
	resp, err := makeReq(url)
	var body globalMarketMedia
	err = json.Unmarshal(resp, &body)
	if err != nil {
		return nil, err
	}
	return body.Data, nil
}

// GlobalMarketGraphOptions options for global market graph method
type GlobalMarketGraphOptions struct {
	Start int64
	End   int64
}

// GlobalMarketGraph get graph data points of global market
func GlobalMarketGraph(options *GlobalMarketGraphOptions) (*types.MarketGraph, error) {
	url := fmt.Sprintf("%s/%d/%d", globalMarketGraphURL, options.Start*1000, options.End*1000)
	resp, err := makeReq(url)
	if err != nil {
		return nil, err
	}
	var data *types.MarketGraph
	err = json.Unmarshal(resp, &data)
	if err != nil {
		return nil, err
	}
	return data, nil
}

// GlobalAltcoinMarketGraphOptions options for global altcoin market graph method
type GlobalAltcoinMarketGraphOptions struct {
	Start int64
	End   int64
}

// GlobalAltcoinMarketGraph gets graph data points of altcoin market
func GlobalAltcoinMarketGraph(options *GlobalAltcoinMarketGraphOptions) (*types.MarketGraph, error) {
	url := fmt.Sprintf("%s/%d/%d", altcoinMarketGraphURL, options.Start*1000, options.End*1000)
	resp, err := makeReq(url)
	if err != nil {
		return nil, err
	}
	var data *types.MarketGraph
	err = json.Unmarshal(resp, &data)
	if err != nil {
		return nil, err
	}
	return data, nil
}

// MarketsOptions options for markets method
type MarketsOptions struct {
	Symbol string
}

// Markets get market data for a cryptocurrency
func Markets(options *MarketsOptions) ([]*types.Market, error) {
	slug, err := CoinSlug(options.Symbol)
	if err != nil {
		return nil, err
	}
	url := fmt.Sprintf("%s/currencies/%s/#markets", siteURL, slug)
	var markets []*types.Market
	response, err := soup.Get(url)
	if err != nil {
		return nil, err
	}
	rows := soup.HTMLParse(response).Find("table", "id", "markets-table").Find("tbody").FindAll("tr")
	for _, row := range rows {
		var data []string
		for _, column := range row.FindAll("td") {
			attrs := column.Attrs()
			if attrs["data-sort"] != "" {
				data = append(data, attrs["data-sort"])
			} else {
				data = append(data, column.Text())
			}
		}
		markets = append(markets, &types.Market{
			Rank:          toInt(data[0]),
			Exchange:      data[1],
			Pair:          data[2],
			VolumeUSD:     toFloat(data[3]),
			Price:         toFloat(data[4]),
			VolumePercent: toFloat(data[5]),
			Updated:       data[6],
		})
	}
	return markets, nil
}

// PriceOptions options for price method
type PriceOptions struct {
	Symbol  string
	Convert string
}

// Price gets price of a cryptocurrency
func Price(options *PriceOptions) (float64, error) {
	coin, err := Ticker(&TickerOptions{
		Convert: options.Convert,
		Symbol:  options.Symbol,
	})
	if err != nil {
		return 0, err
	}

	if coin == nil {
		return 0, errors.New("coin not found")
	}
	return coin.Quotes[options.Convert].Price, nil
}

// CoinID gets the ID for the cryptocurrency
func CoinID(symbol string) (int, error) {
	symbol = strings.ToUpper(strings.TrimSpace(symbol))
	listings, err := Listings()
	if err != nil {
		return 0, err
	}

	for _, l := range listings {
		if l.Symbol == symbol {
			return l.ID, nil
		}

		if l.Slug == strings.ToLower(symbol) {
			return l.ID, nil
		}
	}

	return 0, errors.New("coin not found")
}

// CoinSlug gets the slug for the cryptocurrency
func CoinSlug(symbol string) (string, error) {
	symbol = strings.ToUpper(strings.TrimSpace(symbol))
	coin, err := Ticker(&TickerOptions{
		Symbol: symbol,
	})
	if err != nil {
		return "", err
	}

	if coin == nil {
		return "", errors.New("coin not found")
	}
	return coin.Slug, nil
}

// CoinSymbol gets the symbol for the cryptocurrency
func CoinSymbol(slug string) (string, error) {
	slug = strings.ToLower(strings.TrimSpace(slug))
	coin, err := Ticker(&TickerOptions{
		Symbol: slug,
	})
	if err != nil {
		return "", err
	}

	if coin == nil {
		return "", errors.New("coin not found")
	}

	return coin.Symbol, nil
}

// toInt helper for parsing strings to int
func toInt(rawInt string) int {
	parsed, _ := strconv.Atoi(strings.Replace(strings.Replace(rawInt, "$", "", -1), ",", "", -1))
	return parsed
}

// toFloat helper for parsing strings to float
func toFloat(rawFloat string) float64 {
	parsed, _ := strconv.ParseFloat(strings.Replace(strings.Replace(strings.Replace(rawFloat, "$", "", -1), ",", "", -1), "%", "", -1), 64)
	return parsed
}

// doReq HTTP 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 200 != resp.StatusCode {
		return nil, fmt.Errorf("%s", body)
	}

	return body, nil
}

// makeReq HTTP 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
}
