// Package coinswitchhft — single-file Go client for the CoinSwitch PRO
// HFT REST API.
//
// Base URL: https://dma.coinswitch.co
// Response envelope: {retCode, retMsg, result, retExtInfo, time}.
// Always check retCode == 0 before reading result.
package coinswitchhft

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

	"github.com/google/uuid"
)

const BaseURL = "https://dma.coinswitch.co"

type Client struct {
	APIKey, SecretKey, BaseURL string
	HTTP                       *http.Client
	priv                       ed25519.PrivateKey
}

func NewClient(apiKey, secretKey string) *Client {
	return NewClientWithURL(apiKey, secretKey, BaseURL)
}

func NewClientWithURL(apiKey, secretKey, baseURL string) *Client {
	seed, err := hex.DecodeString(secretKey)
	if err != nil {
		panic(err)
	}
	return &Client{
		APIKey: apiKey, SecretKey: secretKey, BaseURL: baseURL,
		HTTP: &http.Client{Timeout: 5 * time.Second},
		priv: ed25519.NewKeyFromSeed(seed),
	}
}

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 += sep + q.Encode()
	}
	decodedPath, err = url.QueryUnescape(path)
	if err != nil {
		return nil, "", err
	}
	epoch := strconv.FormatInt(time.Now().UnixMilli(), 10)
	signature := hex.EncodeToString(ed25519.Sign(c.priv, []byte(method+decodedPath+epoch)))
	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) ([]byte, error) {
	headers, decoded, err := c.sign(method, path, params)
	if err != nil {
		return nil, err
	}
	var bodyReader io.Reader
	if body != nil {
		j, _ := json.Marshal(body)
		bodyReader = bytes.NewReader(j)
	}
	req, _ := http.NewRequest(strings.ToUpper(method), c.BaseURL+decoded, bodyReader)
	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
}

// ---- Market data ----
func (c *Client) ServerTime() ([]byte, error) {
	return c.request("GET", "/v5/market/time", nil, nil)
}
func (c *Client) InstrumentsInfo(category, symbol string, limit int) ([]byte, error) {
	if category == "" {
		category = "linear"
	}
	p := map[string]string{"category": category}
	if symbol != "" {
		p["symbol"] = symbol
	}
	if limit > 0 {
		p["limit"] = strconv.Itoa(limit)
	}
	return c.request("GET", "/v5/market/instruments-info", p, nil)
}
func (c *Client) Tickers(category, symbol string) ([]byte, error) {
	if category == "" {
		category = "linear"
	}
	p := map[string]string{"category": category}
	if symbol != "" {
		p["symbol"] = symbol
	}
	return c.request("GET", "/v5/market/tickers", p, nil)
}
func (c *Client) OrderBook(category, symbol string, limit int) ([]byte, error) {
	if category == "" {
		category = "linear"
	}
	p := map[string]string{"category": category, "symbol": symbol}
	if limit > 0 {
		p["limit"] = strconv.Itoa(limit)
	}
	return c.request("GET", "/v5/market/orderbook", p, nil)
}

// ---- Orders ----
// PlaceOrder body must include category, symbol, side ("Buy"/"Sell"),
// orderType ("Limit"/"Market"), qty (string), positionIdx (0/1/2). For
// Limit also price (string). We auto-mint orderLinkId (idempotency key).
func (c *Client) PlaceOrder(body map[string]any) ([]byte, error) {
	if _, ok := body["category"]; !ok {
		body["category"] = "linear"
	}
	if _, ok := body["timeInForce"]; !ok {
		body["timeInForce"] = "GTC"
	}
	if _, ok := body["orderLinkId"]; !ok {
		body["orderLinkId"] = uuid.NewString()
	}
	return c.request("POST", "/v5/order/create", nil, body)
}
func (c *Client) OrderRealtime(filters map[string]string) ([]byte, error) {
	p := map[string]string{"category": "linear"}
	for k, v := range filters {
		p[k] = v
	}
	return c.request("GET", "/v5/order/realtime", p, nil)
}
func (c *Client) ExecutionList(filters map[string]string) ([]byte, error) {
	p := map[string]string{"category": "linear"}
	for k, v := range filters {
		p[k] = v
	}
	return c.request("GET", "/v5/execution/list", p, nil)
}

// ---- Positions ----
func (c *Client) Positions(symbol, cursor string) ([]byte, error) {
	p := map[string]string{"category": "linear"}
	if symbol != "" {
		p["symbol"] = symbol
	}
	if cursor != "" {
		p["cursor"] = cursor
	}
	return c.request("GET", "/v5/position/list", p, nil)
}
func (c *Client) SetLeverage(symbol string, buyLev, sellLev float64) ([]byte, error) {
	return c.request("POST", "/v5/position/set-leverage", nil, map[string]any{
		"category": "linear", "symbol": symbol,
		"buyLeverage": fmt.Sprintf("%g", buyLev),
		"sellLeverage": fmt.Sprintf("%g", sellLev),
	})
}

// SwitchPositionMode: mode = 0 (one-way) or 3 (hedge).
func (c *Client) SwitchPositionMode(mode int, coin string) ([]byte, error) {
	if coin == "" {
		coin = "USDT"
	}
	return c.request("POST", "/v5/position/switch-mode", nil, map[string]any{
		"category": "linear", "coin": coin, "mode": mode,
	})
}

// ---- Account ----
// TransferFunds: direction "IN"/"OUT", quoteAsset "USDT" or "INR" (locked
// on first transfer), clientTxnID is your idempotency key.
func (c *Client) TransferFunds(direction string, amount float64,
	quoteAsset, clientTxnID string) ([]byte, error) {
	if quoteAsset == "" {
		quoteAsset = "USDT"
	}
	if clientTxnID == "" {
		clientTxnID = uuid.NewString()
	}
	return c.request("POST", "/dma/api/v1/funds/transfer", nil, map[string]any{
		"direction":     direction,
		"amount":        amount,
		"quote_asset":   quoteAsset,
		"client_txn_id": clientTxnID,
	})
}

// ---- WebSocket signature for HFT private NATS stream ----
func (c *Client) SocketSignature(expiresInSeconds int) ([]byte, error) {
	if expiresInSeconds <= 0 {
		expiresInSeconds = 300
	}
	return c.request("GET", "/dma/api/v1/socket/signature",
		map[string]string{"expires_in_seconds": strconv.Itoa(expiresInSeconds)}, nil)
}
