# frozen_string_literal: true

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

module SynorContract
  # Synor Contract SDK client for Ruby.
  # Smart contract deployment, interaction, and event handling.
  class Client
    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

    # ==================== Contract Deployment ====================

    def deploy(options)
      body = {
        bytecode: options.bytecode
      }
      body[:abi] = options.abi.map(&:to_h) if options.abi
      body[:constructor_args] = options.constructor_args if options.constructor_args
      body[:value] = options.value if options.value
      body[:gas_limit] = options.gas_limit if options.gas_limit
      body[:gas_price] = options.gas_price if options.gas_price
      body[:nonce] = options.nonce if options.nonce

      response = post("/contract/deploy", body)
      parse_deployment_result(response)
    end

    def deploy_create2(options, salt:)
      body = {
        bytecode: options.bytecode,
        salt: salt
      }
      body[:abi] = options.abi.map(&:to_h) if options.abi
      body[:constructor_args] = options.constructor_args if options.constructor_args
      body[:value] = options.value if options.value
      body[:gas_limit] = options.gas_limit if options.gas_limit
      body[:gas_price] = options.gas_price if options.gas_price

      response = post("/contract/deploy/create2", body)
      parse_deployment_result(response)
    end

    def predict_address(bytecode:, salt:, deployer: nil)
      body = { bytecode: bytecode, salt: salt }
      body[:deployer] = deployer if deployer
      response = post("/contract/predict-address", body)
      response["address"]
    end

    # ==================== Contract Interaction ====================

    def call(options)
      body = {
        contract: options.contract,
        method: options.method,
        args: options.args,
        abi: options.abi.map(&:to_h)
      }
      post("/contract/call", body)
    end

    def send(options)
      body = {
        contract: options.contract,
        method: options.method,
        args: options.args,
        abi: options.abi.map(&:to_h)
      }
      body[:value] = options.value if options.value
      body[:gas_limit] = options.gas_limit if options.gas_limit
      body[:gas_price] = options.gas_price if options.gas_price
      body[:nonce] = options.nonce if options.nonce

      response = post("/contract/send", body)
      parse_transaction_result(response)
    end

    # ==================== Events ====================

    def get_events(filter)
      body = { contract: filter.contract }
      body[:event] = filter.event if filter.event
      body[:from_block] = filter.from_block if filter.from_block
      body[:to_block] = filter.to_block if filter.to_block
      body[:topics] = filter.topics if filter.topics
      body[:abi] = filter.abi.map(&:to_h) if filter.abi

      response = post("/contract/events", body)
      response.map { |e| parse_decoded_event(e) }
    end

    def get_logs(contract, from_block: nil, to_block: nil)
      path = "/contract/logs?contract=#{encode(contract)}"
      path += "&from_block=#{from_block}" if from_block
      path += "&to_block=#{to_block}" if to_block

      response = get(path)
      response.map { |l| parse_event_log(l) }
    end

    def decode_logs(logs:, abi:)
      body = {
        logs: logs.map(&:to_h),
        abi: abi.map(&:to_h)
      }
      response = post("/contract/decode-logs", body)
      response.map { |e| parse_decoded_event(e) }
    end

    # ==================== ABI Utilities ====================

    def encode_call(options)
      body = {
        method: options.method,
        args: options.args,
        abi: options.abi.map(&:to_h)
      }
      response = post("/contract/encode", body)
      response["data"]
    end

    def decode_result(options)
      body = {
        data: options.data,
        method: options.method,
        abi: options.abi.map(&:to_h)
      }
      response = post("/contract/decode", body)
      response["result"]
    end

    def get_selector(signature)
      response = get("/contract/selector?signature=#{encode(signature)}")
      response["selector"]
    end

    # ==================== Gas Estimation ====================

    def estimate_gas(options)
      body = {
        contract: options.contract,
        method: options.method,
        args: options.args,
        abi: options.abi.map(&:to_h)
      }
      body[:value] = options.value if options.value

      response = post("/contract/estimate-gas", body)
      parse_gas_estimation(response)
    end

    # ==================== Contract Information ====================

    def get_bytecode(address)
      response = get("/contract/#{encode(address)}/bytecode")
      parse_bytecode_info(response)
    end

    def verify(options)
      body = {
        address: options.address,
        source_code: options.source_code,
        compiler_version: options.compiler_version
      }
      body[:constructor_args] = options.constructor_args if options.constructor_args
      body[:optimization] = options.optimization if options.optimization
      body[:optimization_runs] = options.optimization_runs if options.optimization_runs
      body[:license] = options.license if options.license

      response = post("/contract/verify", body)
      parse_verification_result(response)
    end

    def get_verification_status(address)
      response = get("/contract/#{encode(address)}/verification")
      parse_verification_result(response)
    end

    # ==================== Multicall ====================

    def multicall(requests)
      body = { calls: requests.map(&:to_h) }
      response = post("/contract/multicall", body)
      response.map { |r| parse_multicall_result(r) }
    end

    # ==================== Storage ====================

    def read_storage(options)
      path = "/contract/storage?contract=#{encode(options.contract)}&slot=#{encode(options.slot)}"
      path += "&block=#{options.block_number}" if options.block_number
      response = get(path)
      response["value"]
    end

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

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

    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.to_s)
    end

    def parse_deployment_result(data)
      DeploymentResult.new(
        contract_address: data["contract_address"],
        transaction_hash: data["transaction_hash"],
        deployer: data["deployer"],
        gas_used: data["gas_used"],
        block_number: data["block_number"],
        block_hash: data["block_hash"]
      )
    end

    def parse_transaction_result(data)
      TransactionResult.new(
        transaction_hash: data["transaction_hash"],
        block_number: data["block_number"],
        block_hash: data["block_hash"],
        gas_used: data["gas_used"],
        effective_gas_price: data["effective_gas_price"],
        status: data["status"],
        logs: (data["logs"] || []).map { |l| parse_event_log(l) }
      )
    end

    def parse_event_log(data)
      EventLog.new(
        address: data["address"],
        topics: data["topics"],
        data: data["data"],
        block_number: data["block_number"],
        transaction_hash: data["transaction_hash"],
        log_index: data["log_index"],
        block_hash: data["block_hash"],
        removed: data["removed"]
      )
    end

    def parse_decoded_event(data)
      DecodedEvent.new(
        name: data["name"],
        signature: data["signature"],
        args: data["args"],
        log: data["log"] ? parse_event_log(data["log"]) : nil
      )
    end

    def parse_gas_estimation(data)
      GasEstimation.new(
        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"],
        estimated_cost: data["estimated_cost"]
      )
    end

    def parse_bytecode_info(data)
      BytecodeInfo.new(
        bytecode: data["bytecode"],
        deployed_bytecode: data["deployed_bytecode"],
        size: data["size"],
        is_contract: data["is_contract"]
      )
    end

    def parse_verification_result(data)
      VerificationResult.new(
        verified: data["verified"],
        address: data["address"],
        compiler_version: data["compiler_version"],
        optimization: data["optimization"],
        optimization_runs: data["optimization_runs"],
        license: data["license"],
        abi: data["abi"]&.map { |e| parse_abi_entry(e) },
        source_code: data["source_code"]
      )
    end

    def parse_abi_entry(data)
      AbiEntry.new(
        type: data["type"],
        name: data["name"],
        inputs: data["inputs"]&.map { |p| parse_abi_parameter(p) },
        outputs: data["outputs"]&.map { |p| parse_abi_parameter(p) },
        state_mutability: data["stateMutability"],
        anonymous: data["anonymous"]
      )
    end

    def parse_abi_parameter(data)
      AbiParameter.new(
        name: data["name"],
        type: data["type"],
        indexed: data["indexed"],
        components: data["components"]&.map { |c| parse_abi_parameter(c) }
      )
    end

    def parse_multicall_result(data)
      MulticallResult.new(
        success: data["success"],
        result: data["result"],
        error: data["error"]
      )
    end
  end
end
