package bitstamp

import (
	"bytes"
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/common/crypto"
	"github.com/thrasher-corp/gocryptotrader/currency"
	"github.com/thrasher-corp/gocryptotrader/encoding/json"
	exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
	"github.com/thrasher-corp/gocryptotrader/exchanges/nonce"
	"github.com/thrasher-corp/gocryptotrader/exchanges/order"
	"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
	"github.com/thrasher-corp/gocryptotrader/exchanges/request"
	"github.com/thrasher-corp/gocryptotrader/types"
)

const (
	bitstampAPIURL                = "https://www.bitstamp.net/api"
	tradeBaseURL                  = "https://www.bitstamp.net/trade/"
	bitstampAPIVersion            = "2"
	bitstampAPITicker             = "ticker"
	bitstampAPITickerHourly       = "ticker_hour"
	bitstampAPIOrderbook          = "order_book"
	bitstampAPITransactions       = "transactions"
	bitstampAPIEURUSD             = "eur_usd"
	bitstampAPITradingFees        = "fees/trading"
	bitstampAPIBalance            = "balance"
	bitstampAPIUserTransactions   = "user_transactions"
	bitstampAPIOHLC               = "ohlc"
	bitstampAPIOpenOrders         = "open_orders"
	bitstampAPIOrderStatus        = "order_status"
	bitstampAPICancelOrder        = "cancel_order"
	bitstampAPICancelAllOrders    = "cancel_all_orders"
	bitstampAPIMarket             = "market"
	bitstampAPIWithdrawalRequests = "withdrawal_requests"
	bitstampAPIOpenWithdrawal     = "withdrawal/open"
	bitstampAPIUnconfirmedBitcoin = "unconfirmed_btc"
	bitstampAPITransferToMain     = "transfer-to-main"
	bitstampAPITransferFromMain   = "transfer-from-main"
	bitstampAPIReturnType         = "string"
	bitstampAPITradingPairsInfo   = "trading-pairs-info"
	bitstampAPIWSAuthToken        = "websockets_token"
	bitstampAPIWSTrades           = "live_trades"
	bitstampAPIWSOrders           = "live_orders"
	bitstampAPIWSOrderbook        = "order_book"
	bitstampAPIWSMyOrders         = "my_orders"
	bitstampAPIWSMyTrades         = "my_trades"

	bitstampRateInterval = time.Minute * 10
	bitstampRequestRate  = 8000
)

// Exchange implements exchange.IBotExchange and contains additional specific api methods for interacting with Bitstamp
type Exchange struct {
	exchange.Base
}

// GetFee returns an estimate of fee based on type of transaction
func (e *Exchange) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
	var fee float64

	switch feeBuilder.FeeType {
	case exchange.CryptocurrencyTradeFee:
		tradingFee, err := e.getTradingFee(ctx, feeBuilder)
		if err != nil {
			return 0, fmt.Errorf("error getting trading fee: %w", err)
		}
		fee = tradingFee
	case exchange.CryptocurrencyDepositFee:
		fee = 0
	case exchange.InternationalBankDepositFee:
		fee = getInternationalBankDepositFee(feeBuilder.Amount)
	case exchange.InternationalBankWithdrawalFee:
		fee = getInternationalBankWithdrawalFee(feeBuilder.Amount)
	case exchange.OfflineTradeFee:
		fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
	}
	if fee < 0 {
		fee = 0
	}
	return fee, nil
}

// GetTradingFee returns a trading fee based on a currency
func (e *Exchange) getTradingFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
	tradingFees, err := e.GetAccountTradingFee(ctx, feeBuilder.Pair)
	if err != nil {
		return 0, err
	}
	fees := tradingFees.Fees
	fee := fees.Taker
	if feeBuilder.IsMaker {
		fee = fees.Maker
	}
	return fee / 100 * feeBuilder.PurchasePrice * feeBuilder.Amount, nil
}

// GetAccountTradingFee returns a TradingFee for a pair
func (e *Exchange) GetAccountTradingFee(ctx context.Context, pair currency.Pair) (TradingFees, error) {
	path := bitstampAPITradingFees + "/" + strings.ToLower(pair.String())

	var resp TradingFees
	if pair.IsEmpty() {
		return resp, currency.ErrCurrencyPairEmpty
	}
	err := e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, path, true, nil, &resp)

	return resp, err
}

// GetAccountTradingFees returns a slice of TradingFee
func (e *Exchange) GetAccountTradingFees(ctx context.Context) ([]TradingFees, error) {
	path := bitstampAPITradingFees
	var resp []TradingFees
	err := e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, path, true, nil, &resp)
	return resp, err
}

// getOfflineTradeFee calculates the worst case-scenario trading fee
func getOfflineTradeFee(price, amount float64) float64 {
	return 0.0025 * price * amount
}

// getInternationalBankWithdrawalFee returns international withdrawal fee
func getInternationalBankWithdrawalFee(amount float64) float64 {
	fee := amount * 0.0009

	if fee < 15 {
		return 15
	}
	return fee
}

// getInternationalBankDepositFee returns international deposit fee
func getInternationalBankDepositFee(amount float64) float64 {
	fee := amount * 0.0005

	if fee < 7.5 {
		return 7.5
	}
	if fee > 300 {
		return 300
	}
	return fee
}

// GetTicker returns ticker information
func (e *Exchange) GetTicker(ctx context.Context, symbol string, hourly bool) (*Ticker, error) {
	response := Ticker{}
	tickerEndpoint := bitstampAPITicker
	if hourly {
		tickerEndpoint = bitstampAPITickerHourly
	}
	path := "/v" + bitstampAPIVersion + "/" + tickerEndpoint + "/" + strings.ToLower(symbol) + "/"
	return &response, e.SendHTTPRequest(ctx, exchange.RestSpot, path, &response)
}

// GetOrderbook Returns a JSON dictionary with "bids" and "asks". Each is a list
// of open orders and each order is represented as a list holding the price and
// the amount.
func (e *Exchange) GetOrderbook(ctx context.Context, symbol string) (*Orderbook, error) {
	type response struct {
		Timestamp types.Time                       `json:"timestamp"`
		Bids      orderbook.LevelsArrayPriceAmount `json:"bids"`
		Asks      orderbook.LevelsArrayPriceAmount `json:"asks"`
	}

	path := "/v" + bitstampAPIVersion + "/" + bitstampAPIOrderbook + "/" + strings.ToLower(symbol) + "/"
	var resp response
	if err := e.SendHTTPRequest(ctx, exchange.RestSpot, path, &resp); err != nil {
		return nil, err
	}

	return &Orderbook{Timestamp: resp.Timestamp.Time(), Bids: resp.Bids.Levels(), Asks: resp.Asks.Levels()}, nil
}

// GetTradingPairs returns a list of trading pairs which Bitstamp
// currently supports
func (e *Exchange) GetTradingPairs(ctx context.Context) ([]TradingPair, error) {
	var result []TradingPair
	path := "/v" + bitstampAPIVersion + "/" + bitstampAPITradingPairsInfo
	return result, e.SendHTTPRequest(ctx, exchange.RestSpot, path, &result)
}

// GetTransactions returns transaction information
// value parameter ["time"] = "minute", "hour", "day" will collate your
// response into time intervals.
func (e *Exchange) GetTransactions(ctx context.Context, currencyPair, timePeriod string) ([]Transactions, error) {
	var transactions []Transactions
	requestURL := "/v" + bitstampAPIVersion + "/" + bitstampAPITransactions + "/" + strings.ToLower(currencyPair) + "/"
	if timePeriod != "" {
		requestURL += "?time=" + url.QueryEscape(timePeriod)
	}
	return transactions, e.SendHTTPRequest(ctx, exchange.RestSpot, requestURL, &transactions)
}

// GetEURUSDConversionRate returns the conversion rate between Euro and USD
func (e *Exchange) GetEURUSDConversionRate(ctx context.Context) (EURUSDConversionRate, error) {
	rate := EURUSDConversionRate{}
	path := "/" + bitstampAPIEURUSD
	return rate, e.SendHTTPRequest(ctx, exchange.RestSpot, path, &rate)
}

// GetBalance returns full balance of currency held on the exchange
func (e *Exchange) GetBalance(ctx context.Context) (Balances, error) {
	var balance map[string]types.Number
	err := e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIBalance, true, nil, &balance)
	if err != nil {
		return nil, err
	}
	currs := []string{}
	for k := range balance {
		if strings.HasSuffix(k, "_balance") {
			curr, _, _ := strings.Cut(k, "_")
			currs = append(currs, curr)
		}
	}

	balances := make(map[string]Balance)
	for _, curr := range currs {
		currBalance := Balance{
			Available:     balance[curr+"_available"].Float64(),
			Balance:       balance[curr+"_balance"].Float64(),
			Reserved:      balance[curr+"_reserved"].Float64(),
			WithdrawalFee: balance[curr+"_withdrawal_fee"].Float64(),
		}
		balances[strings.ToUpper(curr)] = currBalance
	}
	return balances, nil
}

// GetUserTransactions returns an array of transactions
func (e *Exchange) GetUserTransactions(ctx context.Context, currencyPair string) ([]UserTransactions, error) {
	var resp []UserTransactions
	var err error
	if currencyPair == "" {
		err = e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIUserTransactions, true, url.Values{}, &resp)
	} else {
		err = e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIUserTransactions+"/"+currencyPair, true, url.Values{}, &resp)
	}
	return resp, err
}

// GetOpenOrders returns all open orders on the exchange
func (e *Exchange) GetOpenOrders(ctx context.Context, currencyPair string) ([]Order, error) {
	var resp []Order
	path := bitstampAPIOpenOrders + "/" + strings.ToLower(currencyPair)
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, path, true, nil, &resp)
}

// GetOrderStatus returns an the status of an order by its ID
func (e *Exchange) GetOrderStatus(ctx context.Context, orderID int64) (OrderStatus, error) {
	resp := OrderStatus{}
	req := url.Values{}
	req.Add("id", strconv.FormatInt(orderID, 10))

	return resp,
		e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIOrderStatus, false, req, &resp)
}

// CancelExistingOrder cancels order by ID
func (e *Exchange) CancelExistingOrder(ctx context.Context, orderID int64) (CancelOrder, error) {
	req := url.Values{}
	req.Add("id", strconv.FormatInt(orderID, 10))

	var result CancelOrder
	err := e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPICancelOrder, true, req, &result)
	if err != nil {
		return result, err
	}

	return result, nil
}

// CancelAllExistingOrders cancels all open orders on the exchange
func (e *Exchange) CancelAllExistingOrders(ctx context.Context) (bool, error) {
	result := false

	return result,
		e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPICancelAllOrders, false, nil, &result)
}

// PlaceOrder places an order on the exchange.
func (e *Exchange) PlaceOrder(ctx context.Context, currencyPair string, price, amount float64, buy, market bool) (Order, error) {
	req := url.Values{}
	req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64))
	req.Add("price", strconv.FormatFloat(price, 'f', -1, 64))
	response := Order{}
	orderType := order.Buy.Lower()

	if !buy {
		orderType = order.Sell.Lower()
	}

	var path string
	if market {
		path = orderType + "/" + bitstampAPIMarket + "/" + strings.ToLower(currencyPair)
	} else {
		path = orderType + "/" + strings.ToLower(currencyPair)
	}

	return response,
		e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, path, true, req, &response)
}

// GetWithdrawalRequests returns withdrawal requests for the account
// timedelta - positive integer with max value 50000000 which returns requests
// from number of seconds ago to now.
func (e *Exchange) GetWithdrawalRequests(ctx context.Context, timedelta int64) ([]WithdrawalRequests, error) {
	var resp []WithdrawalRequests
	if timedelta > 50000000 || timedelta < 0 {
		return resp, errors.New("time delta exceeded, max: 50000000 min: 0")
	}

	value := url.Values{}
	value.Set("timedelta", strconv.FormatInt(timedelta, 10))

	if timedelta == 0 {
		value = url.Values{}
	}

	return resp,
		e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIWithdrawalRequests, false, value, &resp)
}

// CryptoWithdrawal withdraws a cryptocurrency into a supplied wallet, returns ID
// amount - The amount you want withdrawn
// address - The wallet address of the cryptocurrency
// symbol - the type of crypto ie "ltc", "btc", "eth"
// destTag - only for XRP  default to ""
func (e *Exchange) CryptoWithdrawal(ctx context.Context, amount float64, address, symbol, destTag string) (*CryptoWithdrawalResponse, error) {
	req := url.Values{}
	req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64))
	req.Add("address", address)

	var endpoint string
	switch strings.ToUpper(symbol) {
	case currency.XLM.String():
		if destTag != "" {
			req.Add("memo_id", destTag)
		}
	case currency.XRP.String():
		if destTag != "" {
			req.Add("destination_tag", destTag)
		}
	}

	var resp CryptoWithdrawalResponse
	endpoint = strings.ToLower(symbol) + "_withdrawal"
	return &resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, endpoint, true, req, &resp)
}

// OpenBankWithdrawal Opens a bank withdrawal request (SEPA or international)
func (e *Exchange) OpenBankWithdrawal(ctx context.Context, req *OpenBankWithdrawalRequest) (FIATWithdrawalResponse, error) {
	v := url.Values{}
	v.Add("amount", strconv.FormatFloat(req.Amount, 'f', -1, 64))
	v.Add("account_currency", req.Currency.String())
	v.Add("name", req.Name)
	v.Add("iban", req.IBAN)
	v.Add("bic", req.BIC)
	v.Add("address", req.Address)
	v.Add("postal_code", req.PostalCode)
	v.Add("city", req.City)
	v.Add("country", req.Country)
	v.Add("type", req.WithdrawalType)
	v.Add("comment", req.Comment)
	resp := FIATWithdrawalResponse{}
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIOpenWithdrawal, true, v, &resp)
}

// OpenInternationalBankWithdrawal Opens a bank withdrawal request (international)
func (e *Exchange) OpenInternationalBankWithdrawal(ctx context.Context, req *OpenBankWithdrawalRequest) (FIATWithdrawalResponse, error) {
	v := url.Values{}
	v.Add("amount", strconv.FormatFloat(req.Amount, 'f', -1, 64))
	v.Add("account_currency", req.Currency.String())
	v.Add("name", req.Name)
	v.Add("iban", req.IBAN)
	v.Add("bic", req.BIC)
	v.Add("address", req.Address)
	v.Add("postal_code", req.PostalCode)
	v.Add("city", req.City)
	v.Add("country", req.Country)
	v.Add("type", req.WithdrawalType)
	v.Add("comment", req.Comment)
	v.Add("currency", req.InternationalCurrency)
	v.Add("bank_name", req.BankName)
	v.Add("bank_address", req.BankAddress)
	v.Add("bank_postal_code", req.BankPostalCode)
	v.Add("bank_city", req.BankCity)
	v.Add("bank_country", req.BankCountry)
	resp := FIATWithdrawalResponse{}
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIOpenWithdrawal, true, v, &resp)
}

// GetCryptoDepositAddress returns a depositing address by crypto.
// c - example "btc", "ltc", "eth", "xrp" or "bch"
func (e *Exchange) GetCryptoDepositAddress(ctx context.Context, c currency.Code) (*DepositAddress, error) {
	path := c.Lower().String() + "_address"
	var resp DepositAddress
	return &resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, path, true, nil, &resp)
}

// GetUnconfirmedBitcoinDeposits returns unconfirmed transactions
func (e *Exchange) GetUnconfirmedBitcoinDeposits(ctx context.Context) ([]UnconfirmedBTCTransactions, error) {
	var response []UnconfirmedBTCTransactions

	return response,
		e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, bitstampAPIUnconfirmedBitcoin, false, nil, &response)
}

// OHLC returns OHLCV data for step (interval)
func (e *Exchange) OHLC(ctx context.Context, symbol string, start, end time.Time, step, limit string) (resp OHLCResponse, err error) {
	v := url.Values{}
	v.Add("limit", limit)
	v.Add("step", step)

	if start.After(end) && !end.IsZero() {
		return resp, errors.New("start time cannot be after end time")
	}
	if !start.IsZero() {
		v.Add("start", strconv.FormatInt(start.Unix(), 10))
	}
	if !end.IsZero() {
		v.Add("end", strconv.FormatInt(end.Unix(), 10))
	}
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("/v"+bitstampAPIVersion+"/"+bitstampAPIOHLC+"/"+symbol, v), &resp)
}

// TransferAccountBalance transfers funds from either a main or sub account
// amount - to transfers
// currency - which currency to transfer
// subaccount - name of account
// toMain - bool either to or from account
func (e *Exchange) TransferAccountBalance(ctx context.Context, amount float64, ccy, subAccount string, toMain bool) error {
	req := url.Values{}
	req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64))
	req.Add("currency", ccy)

	if subAccount == "" {
		return errors.New("missing subAccount parameter")
	}

	req.Add("subAccount", subAccount)

	var path string
	if toMain {
		path = bitstampAPITransferToMain
	} else {
		path = bitstampAPITransferFromMain
	}

	var resp any

	return e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, path, true, req, &resp)
}

// SendHTTPRequest sends an unauthenticated HTTP request
func (e *Exchange) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, result any) error {
	endpoint, err := e.API.Endpoints.GetURL(ep)
	if err != nil {
		return err
	}
	item := &request.Item{
		Method:                 http.MethodGet,
		Path:                   endpoint + path,
		Result:                 result,
		Verbose:                e.Verbose,
		HTTPDebugging:          e.HTTPDebugging,
		HTTPRecording:          e.HTTPRecording,
		HTTPMockDataSliceLimit: e.HTTPMockDataSliceLimit,
	}
	return e.SendPayload(ctx, request.Unset, func() (*request.Item, error) {
		return item, nil
	}, request.UnauthenticatedRequest)
}

// SendAuthenticatedHTTPRequest sends an authenticated request
func (e *Exchange) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, path string, v2 bool, values url.Values, result any) error {
	creds, err := e.GetCredentials(ctx)
	if err != nil {
		return err
	}
	endpoint, err := e.API.Endpoints.GetURL(ep)
	if err != nil {
		return err
	}

	if values == nil {
		values = url.Values{}
	}

	interim := json.RawMessage{}
	err = e.SendPayload(ctx, request.Unset, func() (*request.Item, error) {
		n := e.Requester.GetNonce(nonce.UnixNano).String()

		values.Set("key", creds.Key)
		values.Set("nonce", n)

		hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte(n+creds.ClientID+creds.Key), []byte(creds.Secret))
		if err != nil {
			return nil, err
		}

		values.Set("signature", strings.ToUpper(hex.EncodeToString(hmac)))

		var fullPath string
		if v2 {
			fullPath = endpoint + "/v" + bitstampAPIVersion + "/" + path + "/"
		} else {
			fullPath = endpoint + "/" + path + "/"
		}

		headers := make(map[string]string)
		headers["Content-Type"] = "application/x-www-form-urlencoded"

		encodedValues := values.Encode()
		readerValues := bytes.NewBufferString(encodedValues)

		return &request.Item{
			Method:                 http.MethodPost,
			Path:                   fullPath,
			Headers:                headers,
			Body:                   readerValues,
			Result:                 &interim,
			NonceEnabled:           true,
			Verbose:                e.Verbose,
			HTTPDebugging:          e.HTTPDebugging,
			HTTPRecording:          e.HTTPRecording,
			HTTPMockDataSliceLimit: e.HTTPMockDataSliceLimit,
		}, nil
	}, request.AuthenticatedRequest)
	if err != nil {
		return err
	}

	errCap := struct {
		Error  string `json:"error"`  // v1 errors
		Status string `json:"status"` // v2 errors
		Reason any    `json:"reason"` // v2 errors
	}{}
	if err := json.Unmarshal(interim, &errCap); err == nil {
		if errCap.Error != "" || errCap.Status == errStr {
			if errCap.Error != "" { // v1 errors
				return fmt.Errorf("%w %v", request.ErrAuthRequestFailed, errCap.Error)
			}
			switch data := errCap.Reason.(type) { // v2 errors
			case map[string]any:
				var details strings.Builder
				for k, v := range data {
					details.WriteString(fmt.Sprintf("%s: %v", k, v))
				}
				return errors.New(details.String())
			case string:
				return errors.New(data)
			default:
				return errors.New(errCap.Status)
			}
		}
	}
	return json.Unmarshal(interim, result)
}

func filterOrderbookZeroBidPrice(ob *orderbook.Book) {
	if len(ob.Bids) == 0 || ob.Bids[len(ob.Bids)-1].Price != 0 {
		return
	}

	ob.Bids = ob.Bids[0 : len(ob.Bids)-1]
}
