# frozen_string_literal: true

require 'net/http'
require 'uri'
require 'json'
require 'base64'

module Synor
  # IBC SDK for Ruby
  #
  # Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability.
  module Ibc
    VERSION = '0.1.0'

    # IBC Height representation
    class Height
      attr_reader :revision_number, :revision_height

      def initialize(revision_number: 0, revision_height: 1)
        @revision_number = revision_number
        @revision_height = revision_height
      end

      def zero?
        @revision_number.zero? && @revision_height.zero?
      end

      def increment
        Height.new(revision_number: @revision_number, revision_height: @revision_height + 1)
      end

      def self.from_json(json)
        Height.new(
          revision_number: json['revision_number'] || 0,
          revision_height: json['revision_height'] || 1
        )
      end

      def to_json(*_args)
        { 'revision_number' => @revision_number, 'revision_height' => @revision_height }
      end
    end

    # Light client types
    module ClientType
      TENDERMINT = 'tendermint'
      SOLO_MACHINE = 'solo_machine'
      LOCALHOST = 'localhost'
      WASM = 'wasm'
    end

    # Trust level configuration
    class TrustLevel
      attr_reader :numerator, :denominator

      def initialize(numerator: 1, denominator: 3)
        @numerator = numerator
        @denominator = denominator
      end

      def self.from_json(json)
        TrustLevel.new(
          numerator: json['numerator'] || 1,
          denominator: json['denominator'] || 3
        )
      end

      def to_json(*_args)
        { 'numerator' => @numerator, 'denominator' => @denominator }
      end
    end

    # Light client state
    class ClientState
      attr_reader :chain_id, :trust_level, :trusting_period, :unbonding_period,
                  :max_clock_drift, :latest_height, :frozen_height

      def initialize(chain_id:, trust_level:, trusting_period:, unbonding_period:,
                     max_clock_drift:, latest_height:, frozen_height: nil)
        @chain_id = chain_id
        @trust_level = trust_level
        @trusting_period = trusting_period
        @unbonding_period = unbonding_period
        @max_clock_drift = max_clock_drift
        @latest_height = latest_height
        @frozen_height = frozen_height
      end

      def self.from_json(json)
        ClientState.new(
          chain_id: json['chain_id'],
          trust_level: TrustLevel.from_json(json['trust_level']),
          trusting_period: json['trusting_period'],
          unbonding_period: json['unbonding_period'],
          max_clock_drift: json['max_clock_drift'],
          latest_height: Height.from_json(json['latest_height']),
          frozen_height: json['frozen_height'] ? Height.from_json(json['frozen_height']) : nil
        )
      end

      def to_json(*_args)
        result = {
          'chain_id' => @chain_id,
          'trust_level' => @trust_level.to_json,
          'trusting_period' => @trusting_period,
          'unbonding_period' => @unbonding_period,
          'max_clock_drift' => @max_clock_drift,
          'latest_height' => @latest_height.to_json
        }
        result['frozen_height'] = @frozen_height.to_json if @frozen_height
        result
      end
    end

    # Connection state
    module ConnectionState
      UNINITIALIZED = 'uninitialized'
      INIT = 'init'
      TRYOPEN = 'tryopen'
      OPEN = 'open'
    end

    # Channel ordering
    module ChannelOrder
      UNORDERED = 'unordered'
      ORDERED = 'ordered'
    end

    # Channel state
    module ChannelState
      UNINITIALIZED = 'uninitialized'
      INIT = 'init'
      TRYOPEN = 'tryopen'
      OPEN = 'open'
      CLOSED = 'closed'
    end

    # Timeout information
    class Timeout
      attr_reader :height, :timestamp

      def initialize(height:, timestamp: 0)
        @height = height
        @timestamp = timestamp
      end

      def self.from_height(h)
        Timeout.new(height: Height.new(revision_height: h))
      end

      def self.from_timestamp(ts)
        Timeout.new(height: Height.new, timestamp: ts)
      end

      def to_json(*_args)
        { 'height' => @height.to_json, 'timestamp' => @timestamp.to_s }
      end
    end

    # Swap state
    module SwapState
      PENDING = 'pending'
      LOCKED = 'locked'
      COMPLETED = 'completed'
      REFUNDED = 'refunded'
      EXPIRED = 'expired'
      CANCELLED = 'cancelled'
    end

    # Atomic swap
    class AtomicSwap
      attr_reader :swap_id, :state, :initiator_htlc, :responder_htlc

      def initialize(swap_id:, state:, initiator_htlc:, responder_htlc: nil)
        @swap_id = swap_id
        @state = state
        @initiator_htlc = initiator_htlc
        @responder_htlc = responder_htlc
      end

      def self.from_json(json)
        AtomicSwap.new(
          swap_id: json['swap_id']['id'],
          state: json['state'],
          initiator_htlc: json['initiator_htlc'],
          responder_htlc: json['responder_htlc']
        )
      end
    end

    # IBC SDK configuration
    class Config
      attr_accessor :api_key, :endpoint, :ws_endpoint, :timeout, :retries, :chain_id, :debug

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

    # IBC exception
    class IbcError < StandardError
      attr_reader :code, :status

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

    # Main IBC client
    class Client
      attr_reader :clients, :connections, :channels, :transfer, :swaps

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

        @clients = LightClientClient.new(self)
        @connections = ConnectionsClient.new(self)
        @channels = ChannelsClient.new(self)
        @transfer = TransferClient.new(self)
        @swaps = SwapsClient.new(self)
      end

      def chain_id
        @config.chain_id
      end

      def get_chain_info
        get('/chain')
      end

      def get_height
        result = get('/chain/height')
        Height.from_json(result)
      end

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

      def close
        @closed = true
      end

      def closed?
        @closed
      end

      # Internal HTTP methods

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

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

      private

      def request(method, path, body = nil)
        raise IbcError.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
                  end

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

        response = http.request(request)

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

        JSON.parse(response.body)
      end
    end

    # Light client sub-client
    class LightClientClient
      def initialize(ibc)
        @ibc = ibc
      end

      def create(client_type:, client_state:, consensus_state:)
        result = @ibc.post('/clients', {
          'client_type' => client_type,
          'client_state' => client_state.to_json,
          'consensus_state' => consensus_state
        })
        result['client_id']
      end

      def get_state(client_id)
        result = @ibc.get("/clients/#{client_id}/state")
        ClientState.from_json(result)
      end

      def list
        result = @ibc.get('/clients')
        result['clients'] || []
      end
    end

    # Connections sub-client
    class ConnectionsClient
      def initialize(ibc)
        @ibc = ibc
      end

      def open_init(client_id:, counterparty_client_id:)
        result = @ibc.post('/connections/init', {
          'client_id' => client_id,
          'counterparty_client_id' => counterparty_client_id
        })
        result['connection_id']
      end

      def get(connection_id)
        @ibc.get("/connections/#{connection_id}")
      end

      def list
        result = @ibc.get('/connections')
        result['connections'] || []
      end
    end

    # Channels sub-client
    class ChannelsClient
      def initialize(ibc)
        @ibc = ibc
      end

      def bind_port(port_id:, module_name:)
        @ibc.post('/ports/bind', { 'port_id' => port_id, 'module' => module_name })
      end

      def open_init(port_id:, ordering:, connection_id:, counterparty_port:, version:)
        result = @ibc.post('/channels/init', {
          'port_id' => port_id,
          'ordering' => ordering,
          'connection_id' => connection_id,
          'counterparty_port' => counterparty_port,
          'version' => version
        })
        result['channel_id']
      end

      def get(port_id:, channel_id:)
        @ibc.get("/channels/#{port_id}/#{channel_id}")
      end

      def list
        result = @ibc.get('/channels')
        result['channels'] || []
      end
    end

    # Transfer sub-client (ICS-20)
    class TransferClient
      def initialize(ibc)
        @ibc = ibc
      end

      def transfer(source_port:, source_channel:, denom:, amount:, sender:, receiver:,
                   timeout: nil, memo: nil)
        body = {
          'source_port' => source_port,
          'source_channel' => source_channel,
          'token' => { 'denom' => denom, 'amount' => amount },
          'sender' => sender,
          'receiver' => receiver
        }
        if timeout
          body['timeout_height'] = timeout.height.to_json
          body['timeout_timestamp'] = timeout.timestamp.to_s
        end
        body['memo'] = memo if memo
        @ibc.post('/transfer', body)
      end

      def get_denom_trace(ibc_denom)
        @ibc.get("/transfer/denom_trace/#{ibc_denom}")
      end
    end

    # Swaps sub-client (HTLC)
    class SwapsClient
      def initialize(ibc)
        @ibc = ibc
      end

      def initiate(responder:, initiator_asset:, responder_asset:)
        @ibc.post('/swaps/initiate', {
          'responder' => responder,
          'initiator_asset' => initiator_asset,
          'responder_asset' => responder_asset
        })
      end

      def lock(swap_id)
        @ibc.post("/swaps/#{swap_id}/lock", {})
      end

      def respond(swap_id, asset)
        @ibc.post("/swaps/#{swap_id}/respond", { 'asset' => asset })
      end

      def claim(swap_id, secret)
        @ibc.post("/swaps/#{swap_id}/claim", {
          'secret' => Base64.strict_encode64(secret.pack('C*'))
        })
      end

      def refund(swap_id)
        @ibc.post("/swaps/#{swap_id}/refund", {})
      end

      def get(swap_id)
        result = @ibc.get("/swaps/#{swap_id}")
        AtomicSwap.from_json(result)
      end

      def list_active
        result = @ibc.get('/swaps/active')
        (result['swaps'] || []).map { |s| AtomicSwap.from_json(s) }
      end
    end
  end
end
