require "base64"
require "digest/sha1"
require "akami/core_ext/hash"
require "akami/xpath_helper"
require "akami/c14n_helper"
require "time"
require "gyoku"

require "akami/wsse/verify_signature"
require "akami/wsse/signature"

module Akami

  # = Akami::WSSE
  #
  # Building Web Service Security.
  class WSSE

    # Namespace for WS Security Secext.
    WSE_NAMESPACE = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"

    # Namespace for WS Security Utility.
    WSU_NAMESPACE = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"

    # PasswordText URI.
    PASSWORD_TEXT_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"

    # PasswordDigest URI.
    PASSWORD_DIGEST_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"

    BASE64_URI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"

    # Returns a value from the WSSE Hash.
    def [](key)
      hash[key]
    end

    # Sets a value on the WSSE Hash.
    def []=(key, value)
      hash[key] = value
    end

    # Sets authentication credentials for a wsse:UsernameToken header.
    # Also accepts whether to use WSSE digest authentication.
    def credentials(username, password, digest = false)
      self.username = username
      self.password = password
      self.digest = digest
    end

    attr_accessor :username, :password, :created_at, :expires_at, :signature, :verify_response

    def sign_with=(klass)
      @signature = klass
    end

    def signature?
      !!@signature
    end

    # Returns whether to use WSSE digest. Defaults to +false+.
    def digest?
      !!@digest
    end

    attr_writer :digest

    # Returns whether to generate a wsse:UsernameToken header.
    def username_token?
      username && password
    end

    # Returns whether to generate a wsu:Timestamp header.
    def timestamp?
      created_at || expires_at || @wsu_timestamp
    end

    # Sets whether to generate a wsu:Timestamp header.
    def timestamp=(timestamp)
      @wsu_timestamp = timestamp
    end

    # Hook for Soap::XML that allows us to add attributes to the env:Body tag
    def body_attributes
      if signature?
        signature.body_attributes
      else
        {}
      end
    end

    # Returns the XML for a WSSE header.
    def to_xml
      if signature? and signature.have_document?
        Gyoku.xml wsse_signature.merge!(hash)
      elsif username_token? && timestamp?
        Gyoku.xml wsse_username_token.merge!(wsu_timestamp) {
          |key, v1, v2| v1.merge!(v2) {
            |key, v1, v2| v1.merge!(v2)
          }
        }
      elsif username_token?
        Gyoku.xml wsse_username_token.merge!(hash)
      elsif timestamp?
        Gyoku.xml wsu_timestamp.merge!(hash)
      else
        ""
      end
    end

  private

    # Returns a Hash containing wsse:UsernameToken details.
    def wsse_username_token
      if digest?
        token = security_hash :wsse, "UsernameToken",
          "wsse:Username" => username,
          "wsse:Nonce" => Base64.encode64(nonce),
          "wsu:Created" => timestamp,
          "wsse:Password" => digest_password,
          :attributes! => { "wsse:Password" => { "Type" => PASSWORD_DIGEST_URI },  "wsse:Nonce" => { "EncodingType" => BASE64_URI } }
        # clear the nonce after each use
        @nonce = nil
      else
        token = security_hash :wsse, "UsernameToken",
          "wsse:Username" => username,
          "wsse:Password" => password,
          :attributes! => { "wsse:Password" => { "Type" => PASSWORD_TEXT_URI } }
      end
      token
    end

    def wsse_signature
      signature_hash = signature.to_token

      # First key/value is tag/hash
      tag, hash = signature_hash.shift

      security_hash nil, tag, hash, signature_hash
    end

    # Returns a Hash containing wsu:Timestamp details.
    def wsu_timestamp
      security_hash :wsu, "Timestamp",
        "wsu:Created" => (created_at || Time.now).utc.xmlschema,
        "wsu:Expires" => (expires_at || (created_at || Time.now) + 60).utc.xmlschema
    end

    # Returns a Hash containing wsse/wsu Security details for a given
    # +namespace+, +tag+ and +hash+.
    def security_hash(namespace, tag, hash, extra_info = {})
      key = [namespace, tag].compact.join(":")

      sec_hash = {
        "wsse:Security" => {
          key => hash
        },
        :attributes! => { "wsse:Security" => { "xmlns:wsse" => WSE_NAMESPACE } }
      }

      unless extra_info.empty?
        sec_hash["wsse:Security"].merge!(extra_info)
      end

      if signature?
        sec_hash[:attributes!].merge!("soapenv:mustUnderstand" => "1")
      else
        sec_hash["wsse:Security"].merge!(:attributes! => { key => { "wsu:Id" => "#{tag}-#{count}", "xmlns:wsu" => WSU_NAMESPACE } })
      end

      sec_hash
    end

    # Returns the WSSE password, encrypted for digest authentication.
    def digest_password
      token = nonce + timestamp + password
      Base64.encode64(Digest::SHA1.digest(token)).chomp!
    end

    # Returns a WSSE nonce.
    def nonce
      @nonce ||= Digest::SHA1.hexdigest random_string + timestamp
    end

    # Returns a random String of 100 characters.
    def random_string
      (0...100).map { ("a".."z").to_a[rand(26)] }.join
    end

    # Returns a WSSE timestamp.
    def timestamp
      @timestamp ||= Time.now.utc.xmlschema
    end

    # Returns a new number with every call.
    def count
      @count ||= 0
      @count += 1
    end

    # Returns a memoized and autovivificating Hash.
    def hash
      @hash ||= Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
    end

  end
end
