# frozen_string_literal: true

require "faraday"
require "json"

module SynorRpc
  # Synor RPC SDK Client
  #
  # @example
  #   client = SynorRpc::Client.new(api_key: 'your-api-key')
  #
  #   # Get latest block
  #   block = client.get_latest_block
  #
  #   # Get transaction
  #   tx = client.get_transaction(txid)
  #
  class Client
    attr_reader :config

    def initialize(api_key: nil, **options)
      @config = Config.new(api_key: api_key, **options)
      raise ArgumentError, "API key is required" unless @config.api_key

      @conn = Faraday.new(url: @config.base_url) do |f|
        f.request :json
        f.response :json
        f.options.timeout = @config.timeout
        f.headers["Authorization"] = "Bearer #{@config.api_key}"
        f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
      end
      @closed = false
      @subscriptions = {}
      @ws = nil
    end

    # ==================== Block Operations ====================

    # Get the latest block
    #
    # @return [Block]
    def get_latest_block
      check_closed!

      response = with_retry { @conn.get("/blocks/latest") }
      Block.from_hash(response.body)
    end

    # Get a block by hash or height
    #
    # @param hash_or_height [String, Integer]
    # @return [Block]
    def get_block(hash_or_height)
      check_closed!

      response = with_retry { @conn.get("/blocks/#{hash_or_height}") }
      Block.from_hash(response.body)
    end

    # Get a block header
    #
    # @param hash_or_height [String, Integer]
    # @return [BlockHeader]
    def get_block_header(hash_or_height)
      check_closed!

      response = with_retry { @conn.get("/blocks/#{hash_or_height}/header") }
      BlockHeader.from_hash(response.body)
    end

    # Get a range of blocks
    #
    # @param start_height [Integer]
    # @param end_height [Integer]
    # @return [Array<Block>]
    def get_blocks(start_height, end_height)
      check_closed!

      response = with_retry do
        @conn.get("/blocks", start: start_height, end: end_height)
      end
      response.body.map { |b| Block.from_hash(b) }
    end

    # ==================== Transaction Operations ====================

    # Get a transaction by ID
    #
    # @param txid [String]
    # @return [Transaction]
    def get_transaction(txid)
      check_closed!

      response = with_retry { @conn.get("/transactions/#{txid}") }
      Transaction.from_hash(response.body)
    end

    # Get raw transaction hex
    #
    # @param txid [String]
    # @return [String]
    def get_raw_transaction(txid)
      check_closed!

      response = with_retry { @conn.get("/transactions/#{txid}/raw") }
      response.body["hex"]
    end

    # Send a raw transaction
    #
    # @param hex [String]
    # @return [SubmitResult]
    def send_raw_transaction(hex)
      check_closed!

      response = with_retry { @conn.post("/transactions/send", { hex: hex }) }
      SubmitResult.from_hash(response.body)
    end

    # Decode a raw transaction
    #
    # @param hex [String]
    # @return [Transaction]
    def decode_raw_transaction(hex)
      check_closed!

      response = with_retry { @conn.post("/transactions/decode", { hex: hex }) }
      Transaction.from_hash(response.body)
    end

    # Get transactions for an address
    #
    # @param address [String]
    # @param limit [Integer]
    # @param offset [Integer]
    # @return [Array<Transaction>]
    def get_address_transactions(address, limit: 20, offset: 0)
      check_closed!

      response = with_retry do
        @conn.get("/addresses/#{address}/transactions", limit: limit, offset: offset)
      end
      response.body.map { |t| Transaction.from_hash(t) }
    end

    # ==================== Fee Estimation ====================

    # Estimate fee
    #
    # @param priority [String]
    # @return [FeeEstimate]
    def estimate_fee(priority: Priority::MEDIUM)
      check_closed!

      response = with_retry { @conn.get("/fees/estimate", priority: priority) }
      FeeEstimate.from_hash(response.body)
    end

    # Get all fee estimates
    #
    # @return [Hash<String, FeeEstimate>]
    def get_all_fee_estimates
      check_closed!

      response = with_retry { @conn.get("/fees/estimates") }
      response.body.to_h { |e| [e["priority"], FeeEstimate.from_hash(e)] }
    end

    # ==================== Chain Information ====================

    # Get chain info
    #
    # @return [ChainInfo]
    def get_chain_info
      check_closed!

      response = with_retry { @conn.get("/chain/info") }
      ChainInfo.from_hash(response.body)
    end

    # Get mempool info
    #
    # @return [MempoolInfo]
    def get_mempool_info
      check_closed!

      response = with_retry { @conn.get("/mempool/info") }
      MempoolInfo.from_hash(response.body)
    end

    # Get mempool transaction IDs
    #
    # @param limit [Integer]
    # @return [Array<String>]
    def get_mempool_transactions(limit: 100)
      check_closed!

      response = with_retry { @conn.get("/mempool/transactions", limit: limit) }
      response.body
    end

    # ==================== Subscriptions ====================

    # Subscribe to new blocks
    #
    # @yield [Block] Called for each new block
    # @return [Subscription]
    def subscribe_blocks(&block)
      check_closed!
      raise ArgumentError, "Block required" unless block_given?

      ensure_websocket_connected

      subscription_id = SecureRandom.uuid
      @subscriptions[subscription_id] = {
        channel: "blocks",
        callback: ->(data) { block.call(Block.from_hash(data)) }
      }

      send_ws_message({
        type: "subscribe",
        channel: "blocks",
        subscription_id: subscription_id
      })

      Subscription.new(id: subscription_id, channel: "blocks") do
        @subscriptions.delete(subscription_id)
        send_ws_message({
          type: "unsubscribe",
          subscription_id: subscription_id
        })
      end
    end

    # Subscribe to address transactions
    #
    # @param address [String]
    # @yield [Transaction] Called for each transaction
    # @return [Subscription]
    def subscribe_address(address, &block)
      check_closed!
      raise ArgumentError, "Block required" unless block_given?

      ensure_websocket_connected

      subscription_id = SecureRandom.uuid
      @subscriptions[subscription_id] = {
        channel: "address:#{address}",
        callback: ->(data) { block.call(Transaction.from_hash(data)) }
      }

      send_ws_message({
        type: "subscribe",
        channel: "address",
        address: address,
        subscription_id: subscription_id
      })

      Subscription.new(id: subscription_id, channel: "address:#{address}") do
        @subscriptions.delete(subscription_id)
        send_ws_message({
          type: "unsubscribe",
          subscription_id: subscription_id
        })
      end
    end

    # Subscribe to mempool transactions
    #
    # @yield [Transaction] Called for each transaction
    # @return [Subscription]
    def subscribe_mempool(&block)
      check_closed!
      raise ArgumentError, "Block required" unless block_given?

      ensure_websocket_connected

      subscription_id = SecureRandom.uuid
      @subscriptions[subscription_id] = {
        channel: "mempool",
        callback: ->(data) { block.call(Transaction.from_hash(data)) }
      }

      send_ws_message({
        type: "subscribe",
        channel: "mempool",
        subscription_id: subscription_id
      })

      Subscription.new(id: subscription_id, channel: "mempool") do
        @subscriptions.delete(subscription_id)
        send_ws_message({
          type: "unsubscribe",
          subscription_id: subscription_id
        })
      end
    end

    # ==================== Lifecycle ====================

    def close
      @closed = true
      @ws&.close
      @conn.close if @conn.respond_to?(:close)
    end

    def closed?
      @closed
    end

    private

    def check_closed!
      raise ClientClosedError, "Client has been closed" if @closed
    end

    def with_retry
      attempts = 0
      begin
        attempts += 1
        response = yield
        handle_response(response)
        response
      rescue Faraday::Error, ApiError => e
        if attempts < @config.retries
          sleep(attempts)
          retry
        end
        raise
      end
    end

    def handle_response(response)
      return if response.success?

      error_message = response.body["error"] || response.body["message"] || "Unknown error"
      raise ApiError.new(error_message, status_code: response.status)
    end

    def ensure_websocket_connected
      return if @ws && @ws_connected

      require "websocket-client-simple"

      @ws = WebSocket::Client::Simple.connect(@config.ws_url, headers: {
        "Authorization" => "Bearer #{@config.api_key}"
      })

      @ws.on :message do |msg|
        handle_ws_message(msg.data)
      end

      @ws.on :open do
        @ws_connected = true
      end

      @ws.on :close do
        @ws_connected = false
      end

      @ws.on :error do |e|
        puts "WebSocket error: #{e.message}" if @config.debug
      end

      # Wait for connection
      sleep(0.1) until @ws_connected || @closed
    end

    def send_ws_message(message)
      @ws&.send(JSON.generate(message))
    end

    def handle_ws_message(data)
      message = JSON.parse(data)
      subscription_id = message["subscription_id"]

      if subscription_id && @subscriptions[subscription_id]
        @subscriptions[subscription_id][:callback].call(message["data"])
      end
    rescue JSON::ParserError
      # Skip malformed messages
    end
  end
end
