# frozen_string_literal: true

require 'net/http'
require 'uri'
require 'json'
require 'websocket-client-simple'

module Synor
  # DEX SDK for Ruby
  #
  # 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
  module Dex
    VERSION = '0.1.0'

    # DEX configuration
    class Config
      attr_accessor :api_key, :endpoint, :ws_endpoint, :timeout, :retries, :debug

      def initialize(
        api_key:,
        endpoint: 'https://dex.synor.io/v1',
        ws_endpoint: 'wss://dex.synor.io/v1/ws',
        timeout: 30,
        retries: 3,
        debug: false
      )
        @api_key = api_key
        @endpoint = endpoint
        @ws_endpoint = ws_endpoint
        @timeout = timeout
        @retries = retries
        @debug = debug
      end
    end

    # DEX exception
    class DexError < StandardError
      attr_reader :code, :status

      def initialize(message, code: nil, status: nil)
        super(message)
        @code = code
        @status = status
      end
    end

    # Main DEX client
    class Client
      attr_reader :perps, :orderbook, :farms

      def initialize(config)
        @config = config
        @closed = false

        @perps = PerpsClient.new(self)
        @orderbook = OrderBookClient.new(self)
        @farms = FarmsClient.new(self)
      end

      # Token Operations

      def get_token(address)
        get("/tokens/#{address}")
      end

      def list_tokens
        get('/tokens')
      end

      def search_tokens(query)
        get("/tokens/search?q=#{URI.encode_www_form_component(query)}")
      end

      # Pool Operations

      def get_pool(token_a, token_b)
        get("/pools/#{token_a}/#{token_b}")
      end

      def get_pool_by_id(pool_id)
        get("/pools/#{pool_id}")
      end

      def list_pools(filter = nil)
        params = []
        if filter
          params << "tokens=#{filter[:tokens].join(',')}" if filter[:tokens]
          params << "min_tvl=#{filter[:min_tvl]}" if filter[:min_tvl]
          params << "min_volume=#{filter[:min_volume_24h]}" if filter[:min_volume_24h]
          params << "verified=#{filter[:verified]}" unless filter[:verified].nil?
          params << "limit=#{filter[:limit]}" if filter[:limit]
          params << "offset=#{filter[:offset]}" if filter[:offset]
        end
        path = params.empty? ? '/pools' : "/pools?#{params.join('&')}"
        get(path)
      end

      # Swap Operations

      def get_quote(params)
        post('/swap/quote', {
          token_in: params[:token_in],
          token_out: params[:token_out],
          amount_in: params[:amount_in].to_s,
          slippage: params[:slippage] || 0.005
        })
      end

      def swap(params)
        deadline = params[:deadline] || (Time.now.to_i + 1200)
        body = {
          token_in: params[:token_in],
          token_out: params[:token_out],
          amount_in: params[:amount_in].to_s,
          min_amount_out: params[:min_amount_out].to_s,
          deadline: deadline
        }
        body[:recipient] = params[:recipient] if params[:recipient]
        post('/swap', body)
      end

      # Liquidity Operations

      def add_liquidity(params)
        deadline = params[:deadline] || (Time.now.to_i + 1200)
        body = {
          token_a: params[:token_a],
          token_b: params[:token_b],
          amount_a: params[:amount_a].to_s,
          amount_b: params[:amount_b].to_s,
          deadline: deadline
        }
        body[:min_amount_a] = params[:min_amount_a].to_s if params[:min_amount_a]
        body[:min_amount_b] = params[:min_amount_b].to_s if params[:min_amount_b]
        post('/liquidity/add', body)
      end

      def remove_liquidity(params)
        deadline = params[:deadline] || (Time.now.to_i + 1200)
        body = {
          pool: params[:pool],
          lp_amount: params[:lp_amount].to_s,
          deadline: deadline
        }
        body[:min_amount_a] = params[:min_amount_a].to_s if params[:min_amount_a]
        body[:min_amount_b] = params[:min_amount_b].to_s if params[:min_amount_b]
        post('/liquidity/remove', body)
      end

      def get_my_positions
        get('/liquidity/positions')
      end

      # Analytics

      def get_price_history(pair, interval, limit: 100)
        get("/analytics/candles/#{pair}?interval=#{interval}&limit=#{limit}")
      end

      def get_trade_history(pair, limit: 50)
        get("/analytics/trades/#{pair}?limit=#{limit}")
      end

      def get_volume_stats
        get('/analytics/volume')
      end

      def get_tvl
        get('/analytics/tvl')
      end

      # Lifecycle

      def health_check
        response = get('/health')
        response['status'] == 'healthy'
      rescue StandardError
        false
      end

      def close
        @closed = true
      end

      def closed?
        @closed
      end

      # Internal methods

      def get(path)
        request(:get, path)
      end

      def post(path, body)
        request(:post, path, body)
      end

      def delete(path)
        request(:delete, path)
      end

      private

      def request(method, path, body = nil)
        raise DexError.new('Client has been closed', code: 'CLIENT_CLOSED') if @closed

        uri = URI.parse("#{@config.endpoint}#{path}")

        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = uri.scheme == 'https'
        http.read_timeout = @config.timeout

        request = case method
                  when :get
                    Net::HTTP::Get.new(uri.request_uri)
                  when :post
                    req = Net::HTTP::Post.new(uri.request_uri)
                    req.body = body.to_json if body
                    req
                  when :delete
                    Net::HTTP::Delete.new(uri.request_uri)
                  end

        request['Content-Type'] = 'application/json'
        request['Authorization'] = "Bearer #{@config.api_key}"
        request['X-SDK-Version'] = 'ruby/0.1.0'

        response = http.request(request)

        unless response.is_a?(Net::HTTPSuccess)
          error = JSON.parse(response.body) rescue {}
          raise DexError.new(
            error['message'] || "HTTP #{response.code}",
            code: error['code'],
            status: response.code.to_i
          )
        end

        JSON.parse(response.body)
      end
    end

    # Perpetual futures sub-client
    class PerpsClient
      def initialize(dex)
        @dex = dex
      end

      def list_markets
        @dex.get('/perps/markets')
      end

      def get_market(symbol)
        @dex.get("/perps/markets/#{symbol}")
      end

      def open_position(params)
        body = {
          market: params[:market],
          side: params[:side].to_s,
          size: params[:size].to_s,
          leverage: params[:leverage],
          order_type: params[:order_type].to_s,
          margin_type: (params[:margin_type] || :cross).to_s,
          reduce_only: params[:reduce_only] || false
        }
        body[:limit_price] = params[:limit_price] if params[:limit_price]
        body[:stop_loss] = params[:stop_loss] if params[:stop_loss]
        body[:take_profit] = params[:take_profit] if params[:take_profit]
        @dex.post('/perps/positions', body)
      end

      def close_position(params)
        body = {
          market: params[:market],
          order_type: (params[:order_type] || :market).to_s
        }
        body[:size] = params[:size].to_s if params[:size]
        body[:limit_price] = params[:limit_price] if params[:limit_price]
        @dex.post('/perps/positions/close', body)
      end

      def get_positions
        @dex.get('/perps/positions')
      end

      def get_position(market)
        @dex.get("/perps/positions/#{market}")
      end

      def get_orders
        @dex.get('/perps/orders')
      end

      def cancel_order(order_id)
        @dex.delete("/perps/orders/#{order_id}")
      end

      def cancel_all_orders(market: nil)
        path = market ? "/perps/orders?market=#{market}" : '/perps/orders'
        result = @dex.delete(path)
        result['cancelled']
      end

      def get_funding_history(market, limit: 100)
        @dex.get("/perps/funding/#{market}?limit=#{limit}")
      end

      def get_funding_rate(market)
        @dex.get("/perps/funding/#{market}/current")
      end
    end

    # Order book sub-client
    class OrderBookClient
      def initialize(dex)
        @dex = dex
      end

      def get_order_book(market, depth: 20)
        @dex.get("/orderbook/#{market}?depth=#{depth}")
      end

      def place_limit_order(params)
        @dex.post('/orderbook/orders', {
          market: params[:market],
          side: params[:side],
          price: params[:price],
          size: params[:size].to_s,
          time_in_force: (params[:time_in_force] || :GTC).to_s,
          post_only: params[:post_only] || false
        })
      end

      def cancel_order(order_id)
        @dex.delete("/orderbook/orders/#{order_id}")
      end

      def get_open_orders(market: nil)
        path = market ? "/orderbook/orders?market=#{market}" : '/orderbook/orders'
        @dex.get(path)
      end

      def get_order_history(limit: 50)
        @dex.get("/orderbook/orders/history?limit=#{limit}")
      end
    end

    # Farms sub-client
    class FarmsClient
      def initialize(dex)
        @dex = dex
      end

      def list_farms
        @dex.get('/farms')
      end

      def get_farm(farm_id)
        @dex.get("/farms/#{farm_id}")
      end

      def stake(params)
        @dex.post('/farms/stake', {
          farm: params[:farm],
          amount: params[:amount].to_s
        })
      end

      def unstake(farm, amount)
        @dex.post('/farms/unstake', {
          farm: farm,
          amount: amount.to_s
        })
      end

      def claim_rewards(farm)
        @dex.post('/farms/claim', { farm: farm })
      end

      def get_my_farm_positions
        @dex.get('/farms/positions')
      end
    end
  end
end
