// Package coinswitchspot provides a single-file Go client for the
// CoinSwitch PRO Spot REST API. Every endpoint is exposed as a method.
//
// Usage:
//
//	cs := coinswitchspot.NewClient("<api key>", "<secret key>")
//	t, _ := cs.ServerTime()
//	fmt.Println(string(t))
//
//	resp, _ := cs.CreateOrder(map[string]any{
//	    "side": "buy", "symbol": "BTC/USDT",
//	    "type": "limit", "price": 60000,
//	    "quantity": 0.001, "exchange": "c2c1",
//	})
//	fmt.Println(string(resp))
package coinswitchspot

import (
	"bytes"
	"crypto/ed25519"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
)

const DefaultBaseURL = "https://coinswitch.co"

type Client struct {
	APIKey    string
	SecretKey string
	BaseURL   string
	HTTP      *http.Client

	priv ed25519.PrivateKey
}

func NewClient(apiKey, secretKey string) *Client {
	seed, err := hex.DecodeString(secretKey)
	if err != nil {
		panic(fmt.Errorf("decode secret: %w", err))
	}
	return &Client{
		APIKey:    apiKey,
		SecretKey: secretKey,
		BaseURL:   DefaultBaseURL,
		HTTP:      &http.Client{Timeout: 10 * time.Second},
		priv:      ed25519.NewKeyFromSeed(seed),
	}
}

// ----------------------------------------------------------------------
// Internals
// ----------------------------------------------------------------------

func (c *Client) sign(method, path string, params map[string]string) (
	headers map[string]string, decodedPath string, err error,
) {
	method = strings.ToUpper(method)
	if len(params) > 0 {
		q := url.Values{}
		for k, v := range params {
			q.Set(k, v)
		}
		sep := "?"
		if strings.Contains(path, "?") {
			sep = "&"
		}
		path = path + sep + q.Encode()
	}
	decodedPath, err = url.QueryUnescape(path)
	if err != nil {
		return nil, "", err
	}

	epoch := strconv.FormatInt(time.Now().UnixMilli(), 10)
	message := method + decodedPath + epoch
	signature := hex.EncodeToString(ed25519.Sign(c.priv, []byte(message)))

	return map[string]string{
		"Content-Type":     "application/json",
		"X-AUTH-APIKEY":    c.APIKey,
		"X-AUTH-SIGNATURE": signature,
		"X-AUTH-EPOCH":     epoch,
	}, decodedPath, nil
}

func (c *Client) request(
	method, path string,
	params map[string]string, body any, signed bool,
) ([]byte, error) {
	var headers map[string]string
	var decoded string
	var err error

	if signed {
		headers, decoded, err = c.sign(method, path, params)
		if err != nil {
			return nil, err
		}
	} else {
		if len(params) > 0 {
			q := url.Values{}
			for k, v := range params {
				q.Set(k, v)
			}
			sep := "?"
			if strings.Contains(path, "?") {
				sep = "&"
			}
			path = path + sep + q.Encode()
		}
		decoded, _ = url.QueryUnescape(path)
		headers = map[string]string{"Content-Type": "application/json"}
	}

	var bodyReader io.Reader
	if body != nil {
		bodyJSON, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		bodyReader = bytes.NewReader(bodyJSON)
	}

	req, err := http.NewRequest(strings.ToUpper(method), c.BaseURL+decoded, bodyReader)
	if err != nil {
		return nil, err
	}
	for k, v := range headers {
		req.Header.Set(k, v)
	}

	resp, err := c.HTTP.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	out, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 400 {
		return out, fmt.Errorf("http %d: %s", resp.StatusCode, string(out))
	}
	return out, nil
}

// ----------------------------------------------------------------------
// Public / health
// ----------------------------------------------------------------------

func (c *Client) ServerTime() ([]byte, error) {
	return c.request("GET", "/trade/api/v2/time", nil, nil, false)
}

func (c *Client) ValidateKeys() ([]byte, error) {
	return c.request("GET", "/trade/api/v2/validate/keys", nil, nil, true)
}

func (c *Client) Ping() ([]byte, error) {
	return c.request("GET", "/trade/api/v2/ping", nil, nil, true)
}

// ----------------------------------------------------------------------
// Reference data
// ----------------------------------------------------------------------

func (c *Client) ActiveCoins(exchange string) ([]byte, error) {
	return c.request("GET", "/trade/api/v2/coins",
		map[string]string{"exchange": exchange}, nil, true)
}

func (c *Client) ExchangePrecision(exchange, symbol string) ([]byte, error) {
	return c.request("POST", "/trade/api/v2/exchangePrecision",
		nil, map[string]any{"exchange": exchange, "symbol": symbol}, true)
}

func (c *Client) TradeInfo(exchange, symbol string) ([]byte, error) {
	p := map[string]string{"exchange": exchange}
	if symbol != "" {
		p["symbol"] = symbol
	}
	return c.request("GET", "/trade/api/v2/tradeInfo", p, nil, true)
}

func (c *Client) TradingFee(exchange string) ([]byte, error) {
	return c.request("GET", "/trade/api/v2/tradingFee",
		map[string]string{"exchange": exchange}, nil, true)
}

// ----------------------------------------------------------------------
// Account
// ----------------------------------------------------------------------

func (c *Client) Portfolio() ([]byte, error) {
	return c.request("GET", "/trade/api/v2/user/portfolio", nil, nil, true)
}

func (c *Client) TDS() ([]byte, error) {
	return c.request("GET", "/trade/api/v2/tds", nil, nil, true)
}

// ----------------------------------------------------------------------
// Orders
// ----------------------------------------------------------------------

// CreateOrder places a spot order. body must include side/symbol/type/
// price/quantity/exchange. We auto-mint a client_order_id (your idempotency
// key) if the caller doesn't include one.
func (c *Client) CreateOrder(body map[string]any) ([]byte, error) {
	if _, ok := body["client_order_id"]; !ok {
		body["client_order_id"] = uuid.NewString()
	}
	return c.request("POST", "/trade/api/v2/order", nil, body, true)
}

func (c *Client) CancelOrder(orderID string) ([]byte, error) {
	return c.request("DELETE", "/trade/api/v2/order", nil,
		map[string]any{"order_id": orderID}, true)
}

func (c *Client) GetOrder(orderID string) ([]byte, error) {
	return c.request("GET", "/trade/api/v2/order",
		map[string]string{"order_id": orderID}, nil, true)
}

// ListOrders supports the full filter set. Pass extra=nil for none.
func (c *Client) ListOrders(open bool, extra map[string]string) ([]byte, error) {
	params := map[string]string{"open": strconv.FormatBool(open)}
	for k, v := range extra {
		params[k] = v
	}
	return c.request("GET", "/trade/api/v2/orders", params, nil, true)
}

// ----------------------------------------------------------------------
// Market data
// ----------------------------------------------------------------------

func (c *Client) Depth(exchange, symbol string) ([]byte, error) {
	return c.request("GET", "/trade/api/v2/depth",
		map[string]string{"exchange": exchange, "symbol": symbol}, nil, true)
}

func (c *Client) Candles(exchange, symbol string, interval int,
	startTime, endTime int64) ([]byte, error) {
	return c.request("GET", "/trade/api/v2/candles", map[string]string{
		"exchange":   exchange,
		"symbol":     symbol,
		"interval":   strconv.Itoa(interval),
		"start_time": strconv.FormatInt(startTime, 10),
		"end_time":   strconv.FormatInt(endTime, 10),
	}, nil, true)
}

func (c *Client) Trades(exchange, symbol string) ([]byte, error) {
	return c.request("GET", "/trade/api/v2/trades",
		map[string]string{"exchange": exchange, "symbol": symbol}, nil, true)
}

func (c *Client) TickerAllPairs(exchange string) ([]byte, error) {
	return c.request("GET", "/trade/api/v2/24hr/all-pairs/ticker",
		map[string]string{"exchange": exchange}, nil, true)
}

func (c *Client) Ticker(symbol, exchange string) ([]byte, error) {
	return c.request("GET", "/trade/api/v2/24hr/ticker",
		map[string]string{
			"symbol":   symbol,
			"exchange": exchange,
		}, nil, true)
}
