/**
 * Synor DEX SDK Client
 * 
 * Complete decentralized exchange client with support for:
 * - AMM swaps (constant product, stable, concentrated)
 * - Liquidity provision
 * - Perpetual futures (up to 100x leverage)
 * - Order books (limit orders)
 * - Farming & staking
 */

import {
  DexConfig,
  Token,
  Pool,
  PoolFilter,
  Quote,
  QuoteParams,
  SwapParams,
  SwapResult,
  AddLiquidityParams,
  RemoveLiquidityParams,
  LiquidityResult,
  LPPosition,
  PerpMarket,
  OpenPositionParams,
  ClosePositionParams,
  PerpPosition,
  PerpOrder,
  ModifyPositionParams,
  FundingPayment,
  OrderBook,
  LimitOrderParams,
  Order,
  Farm,
  StakeParams,
  FarmPosition,
  OHLCV,
  TradeHistory,
  VolumeStats,
  TVLStats,
  Subscription,
  PriceCallback,
  TradeCallback,
  OrderBookCallback,
  PositionCallback,
  DexError,
} from './types';

const DEFAULT_ENDPOINT = 'https://dex.synor.io/v1';
const DEFAULT_WS_ENDPOINT = 'wss://dex.synor.io/v1/ws';
const DEFAULT_TIMEOUT = 30000;
const DEFAULT_RETRIES = 3;

export class SynorDex {
  private config: Required<DexConfig>;
  private closed = false;
  private ws: WebSocket | null = null;
  private subscriptions: Map<string, (data: unknown) => void> = new Map();

  public readonly perps: PerpsClient;
  public readonly orderbook: OrderBookClient;
  public readonly farms: FarmsClient;

  constructor(config: DexConfig) {
    this.config = {
      apiKey: config.apiKey,
      endpoint: config.endpoint || DEFAULT_ENDPOINT,
      wsEndpoint: config.wsEndpoint || DEFAULT_WS_ENDPOINT,
      timeout: config.timeout || DEFAULT_TIMEOUT,
      retries: config.retries || DEFAULT_RETRIES,
      debug: config.debug || false,
    };

    this.perps = new PerpsClient(this);
    this.orderbook = new OrderBookClient(this);
    this.farms = new FarmsClient(this);
  }

  // Token Operations
  async getToken(address: string): Promise<Token> {
    return this.get<Token>('/tokens/' + address);
  }

  async listTokens(): Promise<Token[]> {
    return this.get<Token[]>('/tokens');
  }

  async searchTokens(query: string): Promise<Token[]> {
    return this.get<Token[]>('/tokens/search?q=' + encodeURIComponent(query));
  }

  // Pool Operations
  async getPool(tokenA: string, tokenB: string): Promise<Pool> {
    return this.get<Pool>('/pools/' + tokenA + '/' + tokenB);
  }

  async getPoolById(poolId: string): Promise<Pool> {
    return this.get<Pool>('/pools/' + poolId);
  }

  async listPools(filter?: PoolFilter): Promise<Pool[]> {
    const params = new URLSearchParams();
    if (filter?.tokens) params.set('tokens', filter.tokens.join(','));
    if (filter?.minTvl) params.set('min_tvl', filter.minTvl.toString());
    if (filter?.minVolume24h) params.set('min_volume', filter.minVolume24h.toString());
    if (filter?.verified !== undefined) params.set('verified', String(filter.verified));
    if (filter?.limit) params.set('limit', String(filter.limit));
    if (filter?.offset) params.set('offset', String(filter.offset));
    return this.get<Pool[]>('/pools?' + params.toString());
  }

  // Swap Operations
  async getQuote(params: QuoteParams): Promise<Quote> {
    return this.post<Quote>('/swap/quote', {
      token_in: params.tokenIn,
      token_out: params.tokenOut,
      amount_in: params.amountIn.toString(),
      slippage: params.slippage || 0.005,
    });
  }

  async swap(params: SwapParams): Promise<SwapResult> {
    return this.post<SwapResult>('/swap', {
      token_in: params.tokenIn,
      token_out: params.tokenOut,
      amount_in: params.amountIn.toString(),
      min_amount_out: params.minAmountOut.toString(),
      deadline: params.deadline || Math.floor(Date.now() / 1000) + 1200,
      recipient: params.recipient,
    });
  }

  // Liquidity Operations
  async addLiquidity(params: AddLiquidityParams): Promise<LiquidityResult> {
    return this.post<LiquidityResult>('/liquidity/add', {
      token_a: params.tokenA,
      token_b: params.tokenB,
      amount_a: params.amountA.toString(),
      amount_b: params.amountB.toString(),
      min_amount_a: params.minAmountA?.toString(),
      min_amount_b: params.minAmountB?.toString(),
      deadline: params.deadline || Math.floor(Date.now() / 1000) + 1200,
    });
  }

  async removeLiquidity(params: RemoveLiquidityParams): Promise<LiquidityResult> {
    return this.post<LiquidityResult>('/liquidity/remove', {
      pool: params.pool,
      lp_amount: params.lpAmount.toString(),
      min_amount_a: params.minAmountA?.toString(),
      min_amount_b: params.minAmountB?.toString(),
      deadline: params.deadline || Math.floor(Date.now() / 1000) + 1200,
    });
  }

  async getMyPositions(): Promise<LPPosition[]> {
    return this.get<LPPosition[]>('/liquidity/positions');
  }

  // Analytics
  async getPriceHistory(pair: string, interval: string, limit = 100): Promise<OHLCV[]> {
    return this.get<OHLCV[]>('/analytics/candles/' + pair + '?interval=' + interval + '&limit=' + limit);
  }

  async getTradeHistory(pair: string, limit = 50): Promise<TradeHistory[]> {
    return this.get<TradeHistory[]>('/analytics/trades/' + pair + '?limit=' + limit);
  }

  async getVolumeStats(): Promise<VolumeStats> {
    return this.get<VolumeStats>('/analytics/volume');
  }

  async getTVL(): Promise<TVLStats> {
    return this.get<TVLStats>('/analytics/tvl');
  }

  // Subscriptions
  async subscribePrice(market: string, callback: PriceCallback): Promise<Subscription> {
    return this.subscribe('price', { market }, callback);
  }

  async subscribeTrades(market: string, callback: TradeCallback): Promise<Subscription> {
    return this.subscribe('trades', { market }, callback);
  }

  async subscribeOrderBook(market: string, callback: OrderBookCallback): Promise<Subscription> {
    return this.subscribe('orderbook', { market }, callback);
  }

  // Lifecycle
  async healthCheck(): Promise<boolean> {
    try {
      const response = await this.get<{ status: string }>('/health');
      return response.status === 'healthy';
    } catch {
      return false;
    }
  }

  close(): void {
    this.closed = true;
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
    this.subscriptions.clear();
  }

  // Internal Methods
  async get<T>(path: string): Promise<T> {
    return this.request<T>('GET', path);
  }

  async post<T>(path: string, body: unknown): Promise<T> {
    return this.request<T>('POST', path, body);
  }

  async delete<T>(path: string): Promise<T> {
    return this.request<T>('DELETE', path);
  }

  private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
    if (this.closed) {
      throw new DexError('Client has been closed', 'CLIENT_CLOSED');
    }

    let lastError: Error | null = null;

    for (let attempt = 0; attempt < this.config.retries; attempt++) {
      try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);

        const response = await fetch(this.config.endpoint + path, {
          method,
          headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + this.config.apiKey,
            'X-SDK-Version': 'js/0.1.0',
          },
          body: body ? JSON.stringify(body) : undefined,
          signal: controller.signal,
        });

        clearTimeout(timeoutId);

        if (!response.ok) {
          const error = await response.json().catch(() => ({}));
          throw new DexError(
            error.message || 'HTTP ' + response.status,
            error.code,
            response.status
          );
        }

        return await response.json();
      } catch (error) {
        lastError = error as Error;
        if (this.config.debug) {
          console.error('Attempt ' + (attempt + 1) + ' failed:', error);
        }
        if (attempt < this.config.retries - 1) {
          await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
        }
      }
    }

    throw lastError || new DexError('Unknown error');
  }

  private async subscribe<T>(
    channel: string,
    params: Record<string, unknown>,
    callback: (data: T) => void
  ): Promise<Subscription> {
    await this.ensureWebSocket();

    const subscriptionId = Math.random().toString(36).substring(2);
    this.subscriptions.set(subscriptionId, callback as (data: unknown) => void);

    this.ws!.send(JSON.stringify({
      type: 'subscribe',
      channel,
      subscription_id: subscriptionId,
      ...params,
    }));

    return {
      id: subscriptionId,
      channel,
      cancel: () => {
        this.subscriptions.delete(subscriptionId);
        this.ws?.send(JSON.stringify({
          type: 'unsubscribe',
          subscription_id: subscriptionId,
        }));
      },
    };
  }

  private async ensureWebSocket(): Promise<void> {
    if (this.ws?.readyState === WebSocket.OPEN) return;

    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(this.config.wsEndpoint);
      this.ws.onopen = () => {
        this.ws!.send(JSON.stringify({
          type: 'auth',
          api_key: this.config.apiKey,
        }));
        resolve();
      };
      this.ws.onerror = reject;
      this.ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        if (data.subscription_id && this.subscriptions.has(data.subscription_id)) {
          this.subscriptions.get(data.subscription_id)!(data.data);
        }
      };
    });
  }
}

// Perpetuals Sub-Client
class PerpsClient {
  constructor(private dex: SynorDex) {}

  async listMarkets(): Promise<PerpMarket[]> {
    return this.dex.get<PerpMarket[]>('/perps/markets');
  }

  async getMarket(symbol: string): Promise<PerpMarket> {
    return this.dex.get<PerpMarket>('/perps/markets/' + symbol);
  }

  async openPosition(params: OpenPositionParams): Promise<PerpPosition> {
    return this.dex.post<PerpPosition>('/perps/positions', {
      market: params.market,
      side: params.side,
      size: params.size.toString(),
      leverage: params.leverage,
      order_type: params.orderType,
      limit_price: params.limitPrice,
      stop_loss: params.stopLoss,
      take_profit: params.takeProfit,
      margin_type: params.marginType || 'cross',
      reduce_only: params.reduceOnly || false,
    });
  }

  async closePosition(params: ClosePositionParams): Promise<PerpPosition> {
    return this.dex.post<PerpPosition>('/perps/positions/close', {
      market: params.market,
      size: params.size?.toString(),
      order_type: params.orderType,
      limit_price: params.limitPrice,
    });
  }

  async modifyPosition(params: ModifyPositionParams): Promise<PerpPosition> {
    return this.dex.post<PerpPosition>('/perps/positions/' + params.positionId + '/modify', {
      new_leverage: params.newLeverage,
      new_margin: params.newMargin?.toString(),
      new_stop_loss: params.newStopLoss,
      new_take_profit: params.newTakeProfit,
    });
  }

  async getPositions(): Promise<PerpPosition[]> {
    return this.dex.get<PerpPosition[]>('/perps/positions');
  }

  async getPosition(market: string): Promise<PerpPosition | null> {
    return this.dex.get<PerpPosition | null>('/perps/positions/' + market);
  }

  async getOrders(): Promise<PerpOrder[]> {
    return this.dex.get<PerpOrder[]>('/perps/orders');
  }

  async cancelOrder(orderId: string): Promise<void> {
    await this.dex.delete('/perps/orders/' + orderId);
  }

  async cancelAllOrders(market?: string): Promise<number> {
    const path = market ? '/perps/orders?market=' + market : '/perps/orders';
    const result = await this.dex.delete<{ cancelled: number }>(path);
    return result.cancelled;
  }

  async getFundingHistory(market: string, limit = 100): Promise<FundingPayment[]> {
    return this.dex.get<FundingPayment[]>('/perps/funding/' + market + '?limit=' + limit);
  }

  async getFundingRate(market: string): Promise<{ rate: number; nextTime: number }> {
    return this.dex.get('/perps/funding/' + market + '/current');
  }

  async subscribePosition(callback: PositionCallback): Promise<Subscription> {
    return (this.dex as any).subscribe('position', {}, callback);
  }
}

// Order Book Sub-Client
class OrderBookClient {
  constructor(private dex: SynorDex) {}

  async getOrderBook(market: string, depth = 20): Promise<OrderBook> {
    return this.dex.get<OrderBook>('/orderbook/' + market + '?depth=' + depth);
  }

  async placeLimitOrder(params: LimitOrderParams): Promise<Order> {
    return this.dex.post<Order>('/orderbook/orders', {
      market: params.market,
      side: params.side,
      price: params.price,
      size: params.size.toString(),
      time_in_force: params.timeInForce || 'GTC',
      post_only: params.postOnly || false,
    });
  }

  async cancelOrder(orderId: string): Promise<void> {
    await this.dex.delete('/orderbook/orders/' + orderId);
  }

  async getOpenOrders(market?: string): Promise<Order[]> {
    const path = market ? '/orderbook/orders?market=' + market : '/orderbook/orders';
    return this.dex.get<Order[]>(path);
  }

  async getOrderHistory(limit = 50): Promise<Order[]> {
    return this.dex.get<Order[]>('/orderbook/orders/history?limit=' + limit);
  }
}

// Farms Sub-Client
class FarmsClient {
  constructor(private dex: SynorDex) {}

  async listFarms(): Promise<Farm[]> {
    return this.dex.get<Farm[]>('/farms');
  }

  async getFarm(farmId: string): Promise<Farm> {
    return this.dex.get<Farm>('/farms/' + farmId);
  }

  async stake(params: StakeParams): Promise<FarmPosition> {
    return this.dex.post<FarmPosition>('/farms/stake', {
      farm: params.farm,
      amount: params.amount.toString(),
    });
  }

  async unstake(farm: string, amount: bigint): Promise<FarmPosition> {
    return this.dex.post<FarmPosition>('/farms/unstake', {
      farm,
      amount: amount.toString(),
    });
  }

  async claimRewards(farm: string): Promise<{ amount: bigint; transactionHash: string }> {
    return this.dex.post('/farms/claim', { farm });
  }

  async getMyFarmPositions(): Promise<FarmPosition[]> {
    return this.dex.get<FarmPosition[]>('/farms/positions');
  }
}

export default SynorDex;
