# frozen_string_literal: true

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

module SynorPrivacy
  # Synor Privacy SDK client for Ruby.
  # Confidential transactions, ring signatures, stealth addresses, and cryptographic commitments.
  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

    # ==================== Confidential Transactions ====================

    def create_confidential_tx(inputs:, outputs:)
      body = { inputs: inputs.map(&:to_h), outputs: outputs.map(&:to_h) }
      response = post("/privacy/confidential/create", body)
      parse_confidential_transaction(response)
    end

    def verify_confidential_tx(tx:)
      body = { transaction: tx.to_h }
      response = post("/privacy/confidential/verify", body)
      response["valid"] == true
    end

    def create_commitment(value:, blinding_factor:)
      body = { value: value, blinding_factor: blinding_factor }
      response = post("/privacy/commitment/create", body)
      Commitment.new(
        commitment: response["commitment"],
        blinding_factor: response["blinding_factor"]
      )
    end

    def verify_commitment(commitment:, value:, blinding_factor:)
      body = { commitment: commitment, value: value, blinding_factor: blinding_factor }
      response = post("/privacy/commitment/verify", body)
      response["valid"] == true
    end

    def create_range_proof(value:, blinding_factor:, min_value:, max_value:)
      body = {
        value: value,
        blinding_factor: blinding_factor,
        min_value: min_value,
        max_value: max_value
      }
      response = post("/privacy/range-proof/create", body)
      RangeProof.new(
        proof: response["proof"],
        commitment: response["commitment"],
        min_value: response["min_value"],
        max_value: response["max_value"]
      )
    end

    def verify_range_proof(proof:)
      body = { proof: proof.to_h }
      response = post("/privacy/range-proof/verify", body)
      response["valid"] == true
    end

    # ==================== Ring Signatures ====================

    def create_ring_signature(message:, ring:, signer_index:, private_key:)
      body = {
        message: message,
        ring: ring,
        signer_index: signer_index,
        private_key: private_key
      }
      response = post("/privacy/ring/sign", body)
      RingSignature.new(
        c0: response["c0"],
        s: response["s"],
        key_image: response["key_image"],
        ring: response["ring"]
      )
    end

    def verify_ring_signature(signature:, message:)
      body = { signature: signature.to_h, message: message }
      response = post("/privacy/ring/verify", body)
      response["valid"] == true
    end

    def generate_decoys(count, exclude_key: nil)
      path = "/privacy/ring/decoys?count=#{count}"
      path += "&exclude=#{encode(exclude_key)}" if exclude_key
      get(path)
    end

    def check_key_image(key_image)
      response = get("/privacy/ring/key-image/#{key_image}")
      response["spent"] == true
    end

    # ==================== Stealth Addresses ====================

    def generate_stealth_keypair
      response = post("/privacy/stealth/generate", {})
      StealthKeyPair.new(
        spend_public_key: response["spend_public_key"],
        spend_private_key: response["spend_private_key"],
        view_public_key: response["view_public_key"],
        view_private_key: response["view_private_key"]
      )
    end

    def derive_stealth_address(spend_public_key:, view_public_key:)
      body = { spend_public_key: spend_public_key, view_public_key: view_public_key }
      response = post("/privacy/stealth/derive", body)
      StealthAddress.new(
        address: response["address"],
        ephemeral_public_key: response["ephemeral_public_key"],
        tx_public_key: response["tx_public_key"]
      )
    end

    def recover_stealth_private_key(stealth_address:, view_private_key:, spend_private_key:)
      body = {
        stealth_address: stealth_address,
        view_private_key: view_private_key,
        spend_private_key: spend_private_key
      }
      response = post("/privacy/stealth/recover", body)
      response["private_key"]
    end

    def scan_outputs(view_private_key:, spend_public_key:, from_block:, to_block: nil)
      body = {
        view_private_key: view_private_key,
        spend_public_key: spend_public_key,
        from_block: from_block
      }
      body[:to_block] = to_block if to_block
      response = post("/privacy/stealth/scan", body)
      response.map { |o| parse_stealth_output(o) }
    end

    # ==================== Blinding ====================

    def generate_blinding_factor
      response = post("/privacy/blinding/generate", {})
      response["blinding_factor"]
    end

    def blind_value(value:, blinding_factor:)
      body = { value: value, blinding_factor: blinding_factor }
      response = post("/privacy/blinding/blind", body)
      response["blinded_value"]
    end

    def unblind_value(blinded_value:, blinding_factor:)
      body = { blinded_value: blinded_value, blinding_factor: blinding_factor }
      response = post("/privacy/blinding/unblind", body)
      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)
    end

    def parse_confidential_transaction(data)
      ConfidentialTransaction.new(
        id: data["id"],
        inputs: (data["inputs"] || []).map { |i| parse_confidential_input(i) },
        outputs: (data["outputs"] || []).map { |o| parse_confidential_output(o) },
        fee: data["fee"],
        excess: data["excess"],
        excess_sig: data["excess_sig"],
        kernel_offset: data["kernel_offset"]
      )
    end

    def parse_confidential_input(data)
      ConfidentialTxInput.new(
        commitment: data["commitment"],
        blinding_factor: data["blinding_factor"],
        value: data["value"],
        key_image: data["key_image"]
      )
    end

    def parse_confidential_output(data)
      ConfidentialTxOutput.new(
        commitment: data["commitment"],
        blinding_factor: data["blinding_factor"],
        value: data["value"],
        recipient_public_key: data["recipient_public_key"],
        range_proof: data["range_proof"]
      )
    end

    def parse_stealth_output(data)
      StealthOutput.new(
        tx_hash: data["tx_hash"],
        output_index: data["output_index"],
        stealth_address: data["stealth_address"],
        amount: data["amount"],
        block_height: data["block_height"]
      )
    end
  end
end
