// Package dex provides the Synor DEX SDK for Go
package dex

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"math/big"
	"net/http"
	"net/url"
	"strconv"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/gorilla/websocket"
)

// SynorDex is the main DEX client
type SynorDex struct {
	config        Config
	client        *http.Client
	closed        bool
	ws            *websocket.Conn
	wsMu          sync.Mutex
	subscriptions map[string]func([]byte)
	subMu         sync.RWMutex

	// Sub-clients
	Perps     *PerpsClient
	OrderBook *OrderBookClient
	Farms     *FarmsClient
}

// New creates a new SynorDex client
func New(config Config) *SynorDex {
	if config.Endpoint == "" {
		config.Endpoint = "https://dex.synor.io/v1"
	}
	if config.WSEndpoint == "" {
		config.WSEndpoint = "wss://dex.synor.io/v1/ws"
	}
	if config.Timeout == 0 {
		config.Timeout = 30 * time.Second
	}
	if config.Retries == 0 {
		config.Retries = 3
	}

	dex := &SynorDex{
		config: config,
		client: &http.Client{
			Timeout: config.Timeout,
		},
		subscriptions: make(map[string]func([]byte)),
	}

	dex.Perps = &PerpsClient{dex: dex}
	dex.OrderBook = &OrderBookClient{dex: dex}
	dex.Farms = &FarmsClient{dex: dex}

	return dex
}

// Token Operations

// GetToken gets token information
func (d *SynorDex) GetToken(ctx context.Context, address string) (*Token, error) {
	var token Token
	err := d.get(ctx, "/tokens/"+address, &token)
	return &token, err
}

// ListTokens lists all tokens
func (d *SynorDex) ListTokens(ctx context.Context) ([]Token, error) {
	var tokens []Token
	err := d.get(ctx, "/tokens", &tokens)
	return tokens, err
}

// SearchTokens searches for tokens
func (d *SynorDex) SearchTokens(ctx context.Context, query string) ([]Token, error) {
	var tokens []Token
	err := d.get(ctx, "/tokens/search?q="+url.QueryEscape(query), &tokens)
	return tokens, err
}

// Pool Operations

// GetPool gets a pool by token pair
func (d *SynorDex) GetPool(ctx context.Context, tokenA, tokenB string) (*Pool, error) {
	var pool Pool
	err := d.get(ctx, fmt.Sprintf("/pools/%s/%s", tokenA, tokenB), &pool)
	return &pool, err
}

// GetPoolByID gets a pool by ID
func (d *SynorDex) GetPoolByID(ctx context.Context, poolID string) (*Pool, error) {
	var pool Pool
	err := d.get(ctx, "/pools/"+poolID, &pool)
	return &pool, err
}

// ListPools lists pools with optional filtering
func (d *SynorDex) ListPools(ctx context.Context, filter *PoolFilter) ([]Pool, error) {
	params := url.Values{}
	if filter != nil {
		if len(filter.Tokens) > 0 {
			for _, t := range filter.Tokens {
				params.Add("tokens", t)
			}
		}
		if filter.MinTVL != nil {
			params.Set("min_tvl", strconv.FormatFloat(*filter.MinTVL, 'f', -1, 64))
		}
		if filter.MinVolume24h != nil {
			params.Set("min_volume", strconv.FormatFloat(*filter.MinVolume24h, 'f', -1, 64))
		}
		if filter.Verified != nil {
			params.Set("verified", strconv.FormatBool(*filter.Verified))
		}
		if filter.Limit != nil {
			params.Set("limit", strconv.Itoa(*filter.Limit))
		}
		if filter.Offset != nil {
			params.Set("offset", strconv.Itoa(*filter.Offset))
		}
	}

	var pools []Pool
	path := "/pools"
	if len(params) > 0 {
		path += "?" + params.Encode()
	}
	err := d.get(ctx, path, &pools)
	return pools, err
}

// Swap Operations

// GetQuote gets a swap quote
func (d *SynorDex) GetQuote(ctx context.Context, params QuoteParams) (*Quote, error) {
	slippage := params.Slippage
	if slippage == 0 {
		slippage = 0.005
	}

	body := map[string]interface{}{
		"token_in":  params.TokenIn,
		"token_out": params.TokenOut,
		"amount_in": params.AmountIn.String(),
		"slippage":  slippage,
	}

	var quote Quote
	err := d.post(ctx, "/swap/quote", body, &quote)
	return &quote, err
}

// Swap executes a swap
func (d *SynorDex) Swap(ctx context.Context, params SwapParams) (*SwapResult, error) {
	deadline := time.Now().Unix() + 1200
	if params.Deadline != nil {
		deadline = *params.Deadline
	}

	body := map[string]interface{}{
		"token_in":       params.TokenIn,
		"token_out":      params.TokenOut,
		"amount_in":      params.AmountIn.String(),
		"min_amount_out": params.MinAmountOut.String(),
		"deadline":       deadline,
	}
	if params.Recipient != nil {
		body["recipient"] = *params.Recipient
	}

	var result SwapResult
	err := d.post(ctx, "/swap", body, &result)
	return &result, err
}

// Liquidity Operations

// AddLiquidity adds liquidity to a pool
func (d *SynorDex) AddLiquidity(ctx context.Context, params AddLiquidityParams) (*LiquidityResult, error) {
	deadline := time.Now().Unix() + 1200
	if params.Deadline != nil {
		deadline = *params.Deadline
	}

	body := map[string]interface{}{
		"token_a":  params.TokenA,
		"token_b":  params.TokenB,
		"amount_a": params.AmountA.String(),
		"amount_b": params.AmountB.String(),
		"deadline": deadline,
	}
	if params.MinAmountA != nil {
		body["min_amount_a"] = params.MinAmountA.String()
	}
	if params.MinAmountB != nil {
		body["min_amount_b"] = params.MinAmountB.String()
	}

	var result LiquidityResult
	err := d.post(ctx, "/liquidity/add", body, &result)
	return &result, err
}

// RemoveLiquidity removes liquidity from a pool
func (d *SynorDex) RemoveLiquidity(ctx context.Context, params RemoveLiquidityParams) (*LiquidityResult, error) {
	deadline := time.Now().Unix() + 1200
	if params.Deadline != nil {
		deadline = *params.Deadline
	}

	body := map[string]interface{}{
		"pool":      params.Pool,
		"lp_amount": params.LPAmount.String(),
		"deadline":  deadline,
	}
	if params.MinAmountA != nil {
		body["min_amount_a"] = params.MinAmountA.String()
	}
	if params.MinAmountB != nil {
		body["min_amount_b"] = params.MinAmountB.String()
	}

	var result LiquidityResult
	err := d.post(ctx, "/liquidity/remove", body, &result)
	return &result, err
}

// GetMyPositions gets all LP positions
func (d *SynorDex) GetMyPositions(ctx context.Context) ([]LPPosition, error) {
	var positions []LPPosition
	err := d.get(ctx, "/liquidity/positions", &positions)
	return positions, err
}

// Analytics

// GetPriceHistory gets price history (OHLCV candles)
func (d *SynorDex) GetPriceHistory(ctx context.Context, pair, interval string, limit int) ([]OHLCV, error) {
	var candles []OHLCV
	path := fmt.Sprintf("/analytics/candles/%s?interval=%s&limit=%d", pair, interval, limit)
	err := d.get(ctx, path, &candles)
	return candles, err
}

// GetTradeHistory gets trade history
func (d *SynorDex) GetTradeHistory(ctx context.Context, pair string, limit int) ([]TradeHistory, error) {
	var trades []TradeHistory
	path := fmt.Sprintf("/analytics/trades/%s?limit=%d", pair, limit)
	err := d.get(ctx, path, &trades)
	return trades, err
}

// GetVolumeStats gets volume statistics
func (d *SynorDex) GetVolumeStats(ctx context.Context) (*VolumeStats, error) {
	var stats VolumeStats
	err := d.get(ctx, "/analytics/volume", &stats)
	return &stats, err
}

// GetTVL gets TVL statistics
func (d *SynorDex) GetTVL(ctx context.Context) (*TVLStats, error) {
	var stats TVLStats
	err := d.get(ctx, "/analytics/tvl", &stats)
	return &stats, err
}

// Subscriptions

// SubscribePrice subscribes to price updates
func (d *SynorDex) SubscribePrice(market string, callback PriceCallback) (*Subscription, error) {
	return d.subscribe("price", map[string]interface{}{"market": market}, func(data []byte) {
		var price float64
		if err := json.Unmarshal(data, &price); err == nil {
			callback(price)
		}
	})
}

// SubscribeTrades subscribes to trade updates
func (d *SynorDex) SubscribeTrades(market string, callback TradeCallback) (*Subscription, error) {
	return d.subscribe("trades", map[string]interface{}{"market": market}, func(data []byte) {
		var trade TradeHistory
		if err := json.Unmarshal(data, &trade); err == nil {
			callback(trade)
		}
	})
}

// SubscribeOrderBook subscribes to order book updates
func (d *SynorDex) SubscribeOrderBook(market string, callback OrderBookCallback) (*Subscription, error) {
	return d.subscribe("orderbook", map[string]interface{}{"market": market}, func(data []byte) {
		var book OrderBook
		if err := json.Unmarshal(data, &book); err == nil {
			callback(book)
		}
	})
}

// Lifecycle

// HealthCheck checks if the service is healthy
func (d *SynorDex) HealthCheck(ctx context.Context) bool {
	var result struct {
		Status string `json:"status"`
	}
	if err := d.get(ctx, "/health", &result); err != nil {
		return false
	}
	return result.Status == "healthy"
}

// Close closes the client
func (d *SynorDex) Close() error {
	d.closed = true
	d.wsMu.Lock()
	defer d.wsMu.Unlock()
	if d.ws != nil {
		err := d.ws.Close()
		d.ws = nil
		return err
	}
	return nil
}

// Internal methods

func (d *SynorDex) get(ctx context.Context, path string, result interface{}) error {
	return d.request(ctx, "GET", path, nil, result)
}

func (d *SynorDex) post(ctx context.Context, path string, body interface{}, result interface{}) error {
	return d.request(ctx, "POST", path, body, result)
}

func (d *SynorDex) delete(ctx context.Context, path string, result interface{}) error {
	return d.request(ctx, "DELETE", path, nil, result)
}

func (d *SynorDex) request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
	if d.closed {
		return NewDexError("Client has been closed", "CLIENT_CLOSED", 0)
	}

	var lastErr error

	for attempt := 0; attempt < d.config.Retries; attempt++ {
		var bodyReader io.Reader
		if body != nil {
			jsonBody, err := json.Marshal(body)
			if err != nil {
				return err
			}
			bodyReader = bytes.NewReader(jsonBody)
		}

		req, err := http.NewRequestWithContext(ctx, method, d.config.Endpoint+path, bodyReader)
		if err != nil {
			return err
		}

		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+d.config.APIKey)
		req.Header.Set("X-SDK-Version", "go/0.1.0")

		resp, err := d.client.Do(req)
		if err != nil {
			lastErr = err
			if d.config.Debug {
				fmt.Printf("Attempt %d failed: %v\n", attempt+1, err)
			}
			time.Sleep(time.Duration(1<<attempt) * time.Second)
			continue
		}
		defer resp.Body.Close()

		respBody, err := io.ReadAll(resp.Body)
		if err != nil {
			lastErr = err
			continue
		}

		if resp.StatusCode >= 400 {
			var errResp struct {
				Message string `json:"message"`
				Code    string `json:"code"`
			}
			json.Unmarshal(respBody, &errResp)
			return NewDexError(
				errResp.Message,
				errResp.Code,
				resp.StatusCode,
			)
		}

		if result != nil {
			return json.Unmarshal(respBody, result)
		}
		return nil
	}

	if lastErr != nil {
		return lastErr
	}
	return NewDexError("Unknown error", "", 0)
}

func (d *SynorDex) subscribe(channel string, params map[string]interface{}, callback func([]byte)) (*Subscription, error) {
	if err := d.ensureWebSocket(); err != nil {
		return nil, err
	}

	subscriptionID := uuid.New().String()[:8]

	d.subMu.Lock()
	d.subscriptions[subscriptionID] = callback
	d.subMu.Unlock()

	msg := map[string]interface{}{
		"type":            "subscribe",
		"channel":         channel,
		"subscription_id": subscriptionID,
	}
	for k, v := range params {
		msg[k] = v
	}

	d.wsMu.Lock()
	err := d.ws.WriteJSON(msg)
	d.wsMu.Unlock()
	if err != nil {
		return nil, err
	}

	return &Subscription{
		ID:      subscriptionID,
		Channel: channel,
		Cancel: func() {
			d.subMu.Lock()
			delete(d.subscriptions, subscriptionID)
			d.subMu.Unlock()

			d.wsMu.Lock()
			if d.ws != nil {
				d.ws.WriteJSON(map[string]interface{}{
					"type":            "unsubscribe",
					"subscription_id": subscriptionID,
				})
			}
			d.wsMu.Unlock()
		},
	}, nil
}

func (d *SynorDex) ensureWebSocket() error {
	d.wsMu.Lock()
	defer d.wsMu.Unlock()

	if d.ws != nil {
		return nil
	}

	conn, _, err := websocket.DefaultDialer.Dial(d.config.WSEndpoint, nil)
	if err != nil {
		return err
	}
	d.ws = conn

	// Authenticate
	err = d.ws.WriteJSON(map[string]interface{}{
		"type":    "auth",
		"api_key": d.config.APIKey,
	})
	if err != nil {
		return err
	}

	// Start message handler
	go d.handleMessages()

	return nil
}

func (d *SynorDex) handleMessages() {
	for {
		d.wsMu.Lock()
		ws := d.ws
		d.wsMu.Unlock()

		if ws == nil {
			return
		}

		_, message, err := ws.ReadMessage()
		if err != nil {
			return
		}

		var msg struct {
			SubscriptionID string          `json:"subscription_id"`
			Data           json.RawMessage `json:"data"`
		}
		if err := json.Unmarshal(message, &msg); err != nil {
			continue
		}

		if msg.SubscriptionID != "" {
			d.subMu.RLock()
			callback, ok := d.subscriptions[msg.SubscriptionID]
			d.subMu.RUnlock()
			if ok {
				callback(msg.Data)
			}
		}
	}
}

// PerpsClient handles perpetual futures operations
type PerpsClient struct {
	dex *SynorDex
}

// ListMarkets lists all perpetual markets
func (p *PerpsClient) ListMarkets(ctx context.Context) ([]PerpMarket, error) {
	var markets []PerpMarket
	err := p.dex.get(ctx, "/perps/markets", &markets)
	return markets, err
}

// GetMarket gets a specific perpetual market
func (p *PerpsClient) GetMarket(ctx context.Context, symbol string) (*PerpMarket, error) {
	var market PerpMarket
	err := p.dex.get(ctx, "/perps/markets/"+symbol, &market)
	return &market, err
}

// OpenPosition opens a perpetual position
func (p *PerpsClient) OpenPosition(ctx context.Context, params OpenPositionParams) (*PerpPosition, error) {
	marginType := params.MarginType
	if marginType == "" {
		marginType = MarginTypeCross
	}

	body := map[string]interface{}{
		"market":      params.Market,
		"side":        string(params.Side),
		"size":        params.Size.String(),
		"leverage":    params.Leverage,
		"order_type":  string(params.OrderType),
		"margin_type": string(marginType),
		"reduce_only": params.ReduceOnly,
	}
	if params.LimitPrice != nil {
		body["limit_price"] = *params.LimitPrice
	}
	if params.StopLoss != nil {
		body["stop_loss"] = *params.StopLoss
	}
	if params.TakeProfit != nil {
		body["take_profit"] = *params.TakeProfit
	}

	var position PerpPosition
	err := p.dex.post(ctx, "/perps/positions", body, &position)
	return &position, err
}

// ClosePosition closes a perpetual position
func (p *PerpsClient) ClosePosition(ctx context.Context, params ClosePositionParams) (*PerpPosition, error) {
	body := map[string]interface{}{
		"market":     params.Market,
		"order_type": string(params.OrderType),
	}
	if params.Size != nil {
		body["size"] = params.Size.String()
	}
	if params.LimitPrice != nil {
		body["limit_price"] = *params.LimitPrice
	}

	var position PerpPosition
	err := p.dex.post(ctx, "/perps/positions/close", body, &position)
	return &position, err
}

// ModifyPosition modifies a perpetual position
func (p *PerpsClient) ModifyPosition(ctx context.Context, params ModifyPositionParams) (*PerpPosition, error) {
	body := map[string]interface{}{}
	if params.NewLeverage != nil {
		body["new_leverage"] = *params.NewLeverage
	}
	if params.NewMargin != nil {
		body["new_margin"] = params.NewMargin.String()
	}
	if params.NewStopLoss != nil {
		body["new_stop_loss"] = *params.NewStopLoss
	}
	if params.NewTakeProfit != nil {
		body["new_take_profit"] = *params.NewTakeProfit
	}

	var position PerpPosition
	err := p.dex.post(ctx, "/perps/positions/"+params.PositionID+"/modify", body, &position)
	return &position, err
}

// GetPositions gets all open positions
func (p *PerpsClient) GetPositions(ctx context.Context) ([]PerpPosition, error) {
	var positions []PerpPosition
	err := p.dex.get(ctx, "/perps/positions", &positions)
	return positions, err
}

// GetPosition gets position for a specific market
func (p *PerpsClient) GetPosition(ctx context.Context, market string) (*PerpPosition, error) {
	var position PerpPosition
	err := p.dex.get(ctx, "/perps/positions/"+market, &position)
	return &position, err
}

// GetOrders gets all open orders
func (p *PerpsClient) GetOrders(ctx context.Context) ([]PerpOrder, error) {
	var orders []PerpOrder
	err := p.dex.get(ctx, "/perps/orders", &orders)
	return orders, err
}

// CancelOrder cancels an order
func (p *PerpsClient) CancelOrder(ctx context.Context, orderID string) error {
	return p.dex.delete(ctx, "/perps/orders/"+orderID, nil)
}

// CancelAllOrders cancels all orders
func (p *PerpsClient) CancelAllOrders(ctx context.Context, market *string) (int, error) {
	path := "/perps/orders"
	if market != nil {
		path += "?market=" + *market
	}
	var result struct {
		Cancelled int `json:"cancelled"`
	}
	err := p.dex.delete(ctx, path, &result)
	return result.Cancelled, err
}

// GetFundingHistory gets funding payment history
func (p *PerpsClient) GetFundingHistory(ctx context.Context, market string, limit int) ([]FundingPayment, error) {
	var payments []FundingPayment
	path := fmt.Sprintf("/perps/funding/%s?limit=%d", market, limit)
	err := p.dex.get(ctx, path, &payments)
	return payments, err
}

// GetFundingRate gets current funding rate
func (p *PerpsClient) GetFundingRate(ctx context.Context, market string) (*struct {
	Rate     float64 `json:"rate"`
	NextTime int64   `json:"next_time"`
}, error) {
	var result struct {
		Rate     float64 `json:"rate"`
		NextTime int64   `json:"next_time"`
	}
	err := p.dex.get(ctx, "/perps/funding/"+market+"/current", &result)
	return &result, err
}

// SubscribePosition subscribes to position updates
func (p *PerpsClient) SubscribePosition(callback PositionCallback) (*Subscription, error) {
	return p.dex.subscribe("position", map[string]interface{}{}, func(data []byte) {
		var position PerpPosition
		if err := json.Unmarshal(data, &position); err == nil {
			callback(position)
		}
	})
}

// OrderBookClient handles order book operations
type OrderBookClient struct {
	dex *SynorDex
}

// GetOrderBook gets order book for a market
func (o *OrderBookClient) GetOrderBook(ctx context.Context, market string, depth int) (*OrderBook, error) {
	var book OrderBook
	path := fmt.Sprintf("/orderbook/%s?depth=%d", market, depth)
	err := o.dex.get(ctx, path, &book)
	return &book, err
}

// PlaceLimitOrder places a limit order
func (o *OrderBookClient) PlaceLimitOrder(ctx context.Context, params LimitOrderParams) (*Order, error) {
	tif := params.TimeInForce
	if tif == "" {
		tif = TimeInForceGTC
	}

	body := map[string]interface{}{
		"market":        params.Market,
		"side":          params.Side,
		"price":         params.Price,
		"size":          params.Size.String(),
		"time_in_force": string(tif),
		"post_only":     params.PostOnly,
	}

	var order Order
	err := o.dex.post(ctx, "/orderbook/orders", body, &order)
	return &order, err
}

// CancelOrder cancels an order
func (o *OrderBookClient) CancelOrder(ctx context.Context, orderID string) error {
	return o.dex.delete(ctx, "/orderbook/orders/"+orderID, nil)
}

// GetOpenOrders gets all open orders
func (o *OrderBookClient) GetOpenOrders(ctx context.Context, market *string) ([]Order, error) {
	path := "/orderbook/orders"
	if market != nil {
		path += "?market=" + *market
	}
	var orders []Order
	err := o.dex.get(ctx, path, &orders)
	return orders, err
}

// GetOrderHistory gets order history
func (o *OrderBookClient) GetOrderHistory(ctx context.Context, limit int) ([]Order, error) {
	var orders []Order
	path := fmt.Sprintf("/orderbook/orders/history?limit=%d", limit)
	err := o.dex.get(ctx, path, &orders)
	return orders, err
}

// FarmsClient handles farming operations
type FarmsClient struct {
	dex *SynorDex
}

// ListFarms lists all farms
func (f *FarmsClient) ListFarms(ctx context.Context) ([]Farm, error) {
	var farms []Farm
	err := f.dex.get(ctx, "/farms", &farms)
	return farms, err
}

// GetFarm gets a specific farm
func (f *FarmsClient) GetFarm(ctx context.Context, farmID string) (*Farm, error) {
	var farm Farm
	err := f.dex.get(ctx, "/farms/"+farmID, &farm)
	return &farm, err
}

// Stake stakes tokens in a farm
func (f *FarmsClient) Stake(ctx context.Context, params StakeParams) (*FarmPosition, error) {
	body := map[string]interface{}{
		"farm":   params.Farm,
		"amount": params.Amount.String(),
	}
	var position FarmPosition
	err := f.dex.post(ctx, "/farms/stake", body, &position)
	return &position, err
}

// Unstake unstakes tokens from a farm
func (f *FarmsClient) Unstake(ctx context.Context, farm string, amount *big.Int) (*FarmPosition, error) {
	body := map[string]interface{}{
		"farm":   farm,
		"amount": amount.String(),
	}
	var position FarmPosition
	err := f.dex.post(ctx, "/farms/unstake", body, &position)
	return &position, err
}

// ClaimRewards claims rewards from a farm
func (f *FarmsClient) ClaimRewards(ctx context.Context, farm string) (*struct {
	Amount          *big.Int `json:"amount"`
	TransactionHash string   `json:"transaction_hash"`
}, error) {
	body := map[string]interface{}{
		"farm": farm,
	}
	var result struct {
		Amount          *big.Int `json:"amount"`
		TransactionHash string   `json:"transaction_hash"`
	}
	err := f.dex.post(ctx, "/farms/claim", body, &result)
	return &result, err
}

// GetMyFarmPositions gets all farm positions
func (f *FarmsClient) GetMyFarmPositions(ctx context.Context) ([]FarmPosition, error) {
	var positions []FarmPosition
	err := f.dex.get(ctx, "/farms/positions", &positions)
	return positions, err
}
