# frozen_string_literal: true

require "faraday"
require "json"
require "uri"

module SynorBridge
  # Synor Bridge SDK client for Ruby.
  # Cross-chain asset transfers with lock-mint and burn-unlock patterns.
  class Client
    FINAL_STATUSES = [
      TransferStatus::COMPLETED,
      TransferStatus::FAILED,
      TransferStatus::REFUNDED
    ].freeze

    attr_reader :closed

    def initialize(config)
      @config = config
      @closed = false
      @conn = Faraday.new(url: config.endpoint) do |f|
        f.request :json
        f.response :json
        f.options.timeout = config.timeout
        f.headers["Authorization"] = "Bearer #{config.api_key}"
        f.headers["Content-Type"] = "application/json"
        f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
      end
    end

    # ==================== Chain Operations ====================

    # Get supported chains
    def get_supported_chains
      response = get("/chains")
      (response["chains"] || []).map { |c| parse_chain(c) }
    end

    # Get chain details
    def get_chain(chain_id)
      response = get("/chains/#{chain_id.downcase}")
      parse_chain(response)
    end

    # Check if chain is supported
    def chain_supported?(chain_id)
      chain = get_chain(chain_id)
      chain.supported
    rescue StandardError
      false
    end

    # ==================== Asset Operations ====================

    # Get supported assets for a chain
    def get_supported_assets(chain_id)
      response = get("/chains/#{chain_id.downcase}/assets")
      (response["assets"] || []).map { |a| parse_asset(a) }
    end

    # Get asset details
    def get_asset(asset_id)
      response = get("/assets/#{encode(asset_id)}")
      parse_asset(response)
    end

    # Get wrapped asset info
    def get_wrapped_asset(original_asset_id, target_chain)
      response = get("/assets/#{encode(original_asset_id)}/wrapped/#{target_chain.downcase}")
      parse_wrapped_asset(response)
    end

    # ==================== Fee Operations ====================

    # Estimate transfer fee
    def estimate_fee(asset:, amount:, source_chain:, target_chain:)
      body = {
        asset: asset,
        amount: amount,
        source_chain: source_chain.downcase,
        target_chain: target_chain.downcase
      }
      response = post("/fees/estimate", body)
      parse_fee_estimate(response)
    end

    # Get exchange rate
    def get_exchange_rate(from_asset, to_asset)
      response = get("/rates/#{encode(from_asset)}/#{encode(to_asset)}")
      parse_exchange_rate(response)
    end

    # ==================== Lock-Mint Flow ====================

    # Lock assets for cross-chain transfer
    def lock(asset:, amount:, target_chain:, options: nil)
      body = {
        asset: asset,
        amount: amount,
        target_chain: target_chain.downcase
      }
      if options
        body[:recipient] = options.recipient if options.recipient
        body[:deadline] = options.deadline if options.deadline
        body[:slippage] = options.slippage if options.slippage
      end
      response = post("/transfers/lock", body)
      parse_lock_receipt(response)
    end

    # Get lock proof
    def get_lock_proof(lock_receipt_id)
      response = get("/transfers/lock/#{encode(lock_receipt_id)}/proof")
      parse_lock_proof(response)
    end

    # Wait for lock proof
    def wait_for_lock_proof(lock_receipt_id, poll_interval: 5, max_wait: 600)
      deadline = Time.now + max_wait
      while Time.now < deadline
        begin
          return get_lock_proof(lock_receipt_id)
        rescue HttpError => e
          raise e unless e.confirmations_pending?

          sleep(poll_interval)
        end
      end
      raise HttpError.new("Timeout waiting for lock proof", code: "CONFIRMATIONS_PENDING")
    end

    # Mint wrapped assets
    def mint(proof:, target_address:, options: nil)
      body = { proof: proof_to_hash(proof), target_address: target_address }
      if options
        body[:gas_limit] = options.gas_limit if options.gas_limit
        body[:max_fee_per_gas] = options.max_fee_per_gas if options.max_fee_per_gas
        body[:max_priority_fee_per_gas] = options.max_priority_fee_per_gas if options.max_priority_fee_per_gas
      end
      response = post("/transfers/mint", body)
      parse_signed_transaction(response)
    end

    # ==================== Burn-Unlock Flow ====================

    # Burn wrapped assets
    def burn(wrapped_asset:, amount:, options: nil)
      body = { wrapped_asset: wrapped_asset, amount: amount }
      if options
        body[:recipient] = options.recipient if options.recipient
        body[:deadline] = options.deadline if options.deadline
      end
      response = post("/transfers/burn", body)
      parse_burn_receipt(response)
    end

    # Get burn proof
    def get_burn_proof(burn_receipt_id)
      response = get("/transfers/burn/#{encode(burn_receipt_id)}/proof")
      parse_burn_proof(response)
    end

    # Wait for burn proof
    def wait_for_burn_proof(burn_receipt_id, poll_interval: 5, max_wait: 600)
      deadline = Time.now + max_wait
      while Time.now < deadline
        begin
          return get_burn_proof(burn_receipt_id)
        rescue HttpError => e
          raise e unless e.confirmations_pending?

          sleep(poll_interval)
        end
      end
      raise HttpError.new("Timeout waiting for burn proof", code: "CONFIRMATIONS_PENDING")
    end

    # Unlock original assets
    def unlock(proof:, options: nil)
      body = { proof: burn_proof_to_hash(proof) }
      if options
        body[:gas_limit] = options.gas_limit if options.gas_limit
        body[:gas_price] = options.gas_price if options.gas_price
      end
      response = post("/transfers/unlock", body)
      parse_signed_transaction(response)
    end

    # ==================== Transfer Management ====================

    # Get transfer details
    def get_transfer(transfer_id)
      response = get("/transfers/#{encode(transfer_id)}")
      parse_transfer(response)
    end

    # Get transfer status
    def get_transfer_status(transfer_id)
      transfer = get_transfer(transfer_id)
      transfer.status
    end

    # List transfers
    def list_transfers(filter: nil)
      params = {}
      if filter
        params[:status] = filter.status.downcase if filter.status
        params[:source_chain] = filter.source_chain.downcase if filter.source_chain
        params[:target_chain] = filter.target_chain.downcase if filter.target_chain
        params[:limit] = filter.limit if filter.limit
        params[:offset] = filter.offset if filter.offset
      end
      response = get("/transfers", params)
      (response["transfers"] || []).map { |t| parse_transfer(t) }
    end

    # Wait for transfer completion
    def wait_for_transfer(transfer_id, poll_interval: 10, max_wait: 1800)
      deadline = Time.now + max_wait
      while Time.now < deadline
        transfer = get_transfer(transfer_id)
        return transfer if FINAL_STATUSES.include?(transfer.status)

        sleep(poll_interval)
      end
      raise Error, "Timeout waiting for transfer completion"
    end

    # ==================== Convenience Methods ====================

    # Bridge assets to another chain (lock-mint flow)
    def bridge_to(asset:, amount:, target_chain:, target_address:, lock_options: nil, mint_options: nil)
      receipt = lock(asset: asset, amount: amount, target_chain: target_chain, options: lock_options)
      puts "Locked: #{receipt.id}, waiting for confirmations..." if @config.debug

      proof = wait_for_lock_proof(receipt.id)
      puts "Proof ready, minting on #{target_chain}..." if @config.debug

      mint(proof: proof, target_address: target_address, options: mint_options)
      wait_for_transfer(receipt.id)
    end

    # Bridge assets back to original chain (burn-unlock flow)
    def bridge_back(wrapped_asset:, amount:, burn_options: nil, unlock_options: nil)
      receipt = burn(wrapped_asset: wrapped_asset, amount: amount, options: burn_options)
      puts "Burned: #{receipt.id}, waiting for confirmations..." if @config.debug

      proof = wait_for_burn_proof(receipt.id)
      puts "Proof ready, unlocking on #{receipt.target_chain}..." if @config.debug

      unlock(proof: proof, options: unlock_options)
      wait_for_transfer(receipt.id)
    end

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

    # Health check
    def health_check
      response = get("/health")
      response["status"] == "healthy"
    rescue StandardError
      false
    end

    # Close the client
    def close
      @closed = true
      @conn.close if @conn.respond_to?(:close)
    end

    private

    def get(path, params = {})
      execute { @conn.get(path, params).body }
    end

    def post(path, body)
      execute { @conn.post(path, body).body }
    end

    def execute
      raise ClientClosedError, "Client has been closed" if @closed

      last_error = nil
      @config.retries.times do |attempt|
        begin
          response = yield
          check_error(response) if response.is_a?(Hash)
          return response
        rescue StandardError => e
          last_error = e
          sleep(2**attempt) if attempt < @config.retries - 1
        end
      end
      raise last_error
    end

    def check_error(response)
      return unless response["error"] || (response["code"] && response["message"])

      message = response["message"] || response["error"] || "Unknown error"
      code = response["code"]
      status = response["status_code"] || 0
      raise HttpError.new(message, status_code: status, code: code)
    end

    def encode(str)
      URI.encode_www_form_component(str)
    end

    # Parsing methods

    def parse_chain(data)
      return nil unless data

      Chain.new(
        id: data["id"],
        name: data["name"],
        chain_id: data["chain_id"],
        rpc_url: data["rpc_url"],
        explorer_url: data["explorer_url"],
        native_currency: data["native_currency"] ? NativeCurrency.new(
          name: data["native_currency"]["name"],
          symbol: data["native_currency"]["symbol"],
          decimals: data["native_currency"]["decimals"]
        ) : nil,
        confirmations: data["confirmations"],
        estimated_block_time: data["estimated_block_time"],
        supported: data["supported"]
      )
    end

    def parse_asset(data)
      return nil unless data

      Asset.new(
        id: data["id"],
        symbol: data["symbol"],
        name: data["name"],
        type: data["type"],
        chain: data["chain"],
        contract_address: data["contract_address"],
        decimals: data["decimals"],
        logo_url: data["logo_url"],
        verified: data["verified"]
      )
    end

    def parse_wrapped_asset(data)
      return nil unless data

      WrappedAsset.new(
        original_asset: parse_asset(data["original_asset"]),
        wrapped_asset: parse_asset(data["wrapped_asset"]),
        chain: data["chain"],
        bridge_contract: data["bridge_contract"]
      )
    end

    def parse_lock_receipt(data)
      return nil unless data

      LockReceipt.new(
        id: data["id"],
        tx_hash: data["tx_hash"],
        source_chain: data["source_chain"],
        target_chain: data["target_chain"],
        asset: parse_asset(data["asset"]),
        amount: data["amount"],
        sender: data["sender"],
        recipient: data["recipient"],
        lock_timestamp: data["lock_timestamp"],
        confirmations: data["confirmations"],
        required_confirmations: data["required_confirmations"]
      )
    end

    def parse_lock_proof(data)
      return nil unless data

      LockProof.new(
        lock_receipt: parse_lock_receipt(data["lock_receipt"]),
        merkle_proof: data["merkle_proof"],
        block_header: data["block_header"],
        signatures: (data["signatures"] || []).map do |s|
          ValidatorSignature.new(
            validator: s["validator"],
            signature: s["signature"],
            timestamp: s["timestamp"]
          )
        end
      )
    end

    def parse_burn_receipt(data)
      return nil unless data

      BurnReceipt.new(
        id: data["id"],
        tx_hash: data["tx_hash"],
        source_chain: data["source_chain"],
        target_chain: data["target_chain"],
        wrapped_asset: parse_asset(data["wrapped_asset"]),
        original_asset: parse_asset(data["original_asset"]),
        amount: data["amount"],
        sender: data["sender"],
        recipient: data["recipient"],
        burn_timestamp: data["burn_timestamp"],
        confirmations: data["confirmations"],
        required_confirmations: data["required_confirmations"]
      )
    end

    def parse_burn_proof(data)
      return nil unless data

      BurnProof.new(
        burn_receipt: parse_burn_receipt(data["burn_receipt"]),
        merkle_proof: data["merkle_proof"],
        block_header: data["block_header"],
        signatures: (data["signatures"] || []).map do |s|
          ValidatorSignature.new(
            validator: s["validator"],
            signature: s["signature"],
            timestamp: s["timestamp"]
          )
        end
      )
    end

    def parse_transfer(data)
      return nil unless data

      Transfer.new(
        id: data["id"],
        direction: data["direction"],
        status: data["status"],
        source_chain: data["source_chain"],
        target_chain: data["target_chain"],
        asset: parse_asset(data["asset"]),
        amount: data["amount"],
        sender: data["sender"],
        recipient: data["recipient"],
        source_tx_hash: data["source_tx_hash"],
        target_tx_hash: data["target_tx_hash"],
        fee: data["fee"],
        fee_asset: parse_asset(data["fee_asset"]),
        created_at: data["created_at"],
        updated_at: data["updated_at"],
        completed_at: data["completed_at"],
        error_message: data["error_message"]
      )
    end

    def parse_fee_estimate(data)
      return nil unless data

      FeeEstimate.new(
        bridge_fee: data["bridge_fee"],
        gas_fee_source: data["gas_fee_source"],
        gas_fee_target: data["gas_fee_target"],
        total_fee: data["total_fee"],
        fee_asset: parse_asset(data["fee_asset"]),
        estimated_time: data["estimated_time"],
        exchange_rate: data["exchange_rate"]
      )
    end

    def parse_exchange_rate(data)
      return nil unless data

      ExchangeRate.new(
        from_asset: parse_asset(data["from_asset"]),
        to_asset: parse_asset(data["to_asset"]),
        rate: data["rate"],
        inverse_rate: data["inverse_rate"],
        last_updated: data["last_updated"],
        source: data["source"]
      )
    end

    def parse_signed_transaction(data)
      return nil unless data

      SignedTransaction.new(
        tx_hash: data["tx_hash"],
        chain: data["chain"],
        from: data["from"],
        to: data["to"],
        value: data["value"],
        data: data["data"],
        gas_limit: data["gas_limit"],
        gas_price: data["gas_price"],
        max_fee_per_gas: data["max_fee_per_gas"],
        max_priority_fee_per_gas: data["max_priority_fee_per_gas"],
        nonce: data["nonce"],
        signature: data["signature"]
      )
    end

    # Conversion methods

    def proof_to_hash(proof)
      {
        lock_receipt: lock_receipt_to_hash(proof.lock_receipt),
        merkle_proof: proof.merkle_proof,
        block_header: proof.block_header,
        signatures: proof.signatures.map { |s| signature_to_hash(s) }
      }
    end

    def lock_receipt_to_hash(receipt)
      {
        id: receipt.id,
        tx_hash: receipt.tx_hash,
        source_chain: receipt.source_chain,
        target_chain: receipt.target_chain,
        asset: asset_to_hash(receipt.asset),
        amount: receipt.amount,
        sender: receipt.sender,
        recipient: receipt.recipient,
        lock_timestamp: receipt.lock_timestamp,
        confirmations: receipt.confirmations,
        required_confirmations: receipt.required_confirmations
      }
    end

    def burn_proof_to_hash(proof)
      {
        burn_receipt: burn_receipt_to_hash(proof.burn_receipt),
        merkle_proof: proof.merkle_proof,
        block_header: proof.block_header,
        signatures: proof.signatures.map { |s| signature_to_hash(s) }
      }
    end

    def burn_receipt_to_hash(receipt)
      {
        id: receipt.id,
        tx_hash: receipt.tx_hash,
        source_chain: receipt.source_chain,
        target_chain: receipt.target_chain,
        wrapped_asset: asset_to_hash(receipt.wrapped_asset),
        original_asset: asset_to_hash(receipt.original_asset),
        amount: receipt.amount,
        sender: receipt.sender,
        recipient: receipt.recipient,
        burn_timestamp: receipt.burn_timestamp,
        confirmations: receipt.confirmations,
        required_confirmations: receipt.required_confirmations
      }
    end

    def asset_to_hash(asset)
      return nil unless asset

      {
        id: asset.id,
        symbol: asset.symbol,
        name: asset.name,
        type: asset.type,
        chain: asset.chain,
        contract_address: asset.contract_address,
        decimals: asset.decimals,
        logo_url: asset.logo_url,
        verified: asset.verified
      }
    end

    def signature_to_hash(sig)
      {
        validator: sig.validator,
        signature: sig.signature,
        timestamp: sig.timestamp
      }
    end
  end
end
