require 'savon'

# Note: Full data schema can be found at:
#   https://cert.api2.heartlandportico.com/Hps.Exchange.PosGateway/POSGatewayService.asmx?schema=schema1

# Additional Note: No idea what schema version that's for, or if it ever returns
# anything except latest :(

class HeartlandPortico
  attr_reader :credentials
  def initialize(credentials, test=false)
    @credentials = credentials
    @test = test
  end

  SUPPORTED_REQUEST_METHODS = %w(
    check_sale
    check_void
    credit_account_verify
    credit_add_to_batch
    credit_auth
    credit_return
    credit_reversal
    credit_sale
    credit_void
  )

  SUPPORTED_REQUEST_METHODS.each do |method|
    define_method method do |params|
      client_call(method, params)
    end
  end

private
  def camelize(string)
    # Couldn't get ActiveSupport to load standalone, rolling my own
    string.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
  end

  def client
    Savon.client do |savon|
      savon.wsdl wsdl_path
      savon.soap_version 2
      savon.endpoint endpoint

      # savon.log_level :debug
      # savon.log true

      savon.convert_request_keys_to :camelcase
      savon.pretty_print_xml true
    end
  end

  def client_call(operation, params)
    message = xml_message(operation) do |xml|
      if pin_block?(operation)
        xml.tag!("tns:Block1") do
          xml_build(xml, params)
        end
      else
        xml_build(xml, params)
      end
    end
    Response.new(operation, client.call(:do_transaction, :message_tag => 'PosRequest', :message => message))
  end

  def endpoint
    if @test
      "https://cert.api2.heartlandportico.com/Hps.Exchange.PosGateway/POSGatewayService.asmx"
    else
      "https://api2.heartlandportico.com/Hps.Exchange.PosGateway/PosGatewayService.asmx"
    end
  end

  def pin_block?(operation)
    %w(
      check_sale
      check_void
      credit_account_verify
      credit_auth
      credit_sale
      credit_return
      credit_reversal
    ).include? operation.to_s
  end

  # Loaded live in production, or from a local cache for testing
  def wsdl_path
    if @test
      File.expand_path(File.join(__FILE__, '..', 'wsdl.xml'))
    else
      "#{endpoint}?wsdl=wsdl1"
    end
  end

  def xml_build(xml, values)
    values.keys.sort_by{|k|k.to_s}.each do |key|
      value = values[key]
      tag = "tns:#{camelize(key)}"

      if value.to_s.empty?
        # Skip empty values
      elsif value.kind_of? Hash
        xml.tag!(tag) { xml_build(xml, value) }
      else
        xml.tag!(tag, value)
      end
    end
  end

  def xml_message(operation)
    xml = Builder::XmlMarkup.new
    xml.tag!("tns:Ver1.0") do
      xml.tag!("tns:Header") do
        xml_build(xml, credentials)
      end
      xml.tag!("tns:Transaction") do
        xml.tag!("tns:#{camelize(operation)}") do
          yield xml
        end
      end
    end
  end

  class Response
    attr_reader :body, :operation
    def initialize(operation, savon_response)
      @operation = operation.to_sym
      @body = savon_response.body[:pos_response][:ver1_0]
    end

    def authorization
      body[:header][:gateway_txn_id] if success?
    end

    def message
      return "ERROR" unless successful_request?
      # CreditAddToBatch has an explicit empty response, with hard errors if not successful
      return "APPROVAL" if body[:transaction].has_key?(operation) and body[:transaction][operation].nil?
      response[:rsp_text] || response[:rsp_message]
    end

    def success?
      ["APPROVAL", "CARD OK", "Transaction Approved"].include? message
    end

    def successful_request?
      body[:header][:gateway_rsp_msg] == "Success"
    end

    def avs_code
      response[:avs_rslt_code]
    end
    def avs_address
      %w(A Y X B D M).include? avs_code
    end
    def avs_zip
      %w(Z W Y X D M P).include? avs_code
    end

    def cvv_code
      response[:cvv_rslt_code]
    end

    def cvv
      # Valid Values:
      # M: Match
      # N: No Match
      # P: Not Processed
      # S: Should have been present
      # U: Issuer not certified
      cvv_code == "M"
    end

    def response
      # Yes, I know that's a lot of edge case handling, but I _really_
      # want that to be a hash.
      body[:transaction][operation] || {} rescue {}
    end
    alias params response
  end
end