require "xml_security"
require "onelogin/ruby-saml/attribute_service"
require "onelogin/ruby-saml/utils"
require "onelogin/ruby-saml/validation_error"

# Only supports SAML 2.0
module OneLogin
  module RubySaml

    # SAML2 Toolkit Settings
    #
    class Settings
      def initialize(overrides = {}, keep_security_attributes = false)
        if keep_security_attributes
          security_attributes = overrides.delete(:security) || {}
          config = DEFAULTS.merge(overrides)
          config[:security] = DEFAULTS[:security].merge(security_attributes)
        else
          config = DEFAULTS.merge(overrides)
        end

        config.each do |k,v|
          acc = "#{k.to_s}=".to_sym
          if respond_to? acc
            value = v.is_a?(Hash) ? v.dup : v
            send(acc, value)
          end
        end
        @attribute_consuming_service = AttributeService.new
      end

      # IdP Data
      attr_accessor :idp_entity_id
      attr_writer   :idp_sso_service_url
      attr_writer   :idp_slo_service_url
      attr_accessor :idp_slo_response_service_url
      attr_accessor :idp_cert
      attr_accessor :idp_cert_fingerprint
      attr_accessor :idp_cert_fingerprint_algorithm
      attr_accessor :idp_cert_multi
      attr_accessor :idp_attribute_names
      attr_accessor :idp_name_qualifier
      attr_accessor :valid_until
      # SP Data
      attr_writer   :sp_entity_id
      attr_accessor :assertion_consumer_service_url
      attr_reader   :assertion_consumer_service_binding
      attr_writer   :single_logout_service_url
      attr_accessor :sp_name_qualifier
      attr_accessor :name_identifier_format
      attr_accessor :name_identifier_value
      attr_accessor :name_identifier_value_requested
      attr_accessor :sessionindex
      attr_accessor :compress_request
      attr_accessor :compress_response
      attr_accessor :double_quote_xml_attribute_values
      attr_accessor :message_max_bytesize
      attr_accessor :passive
      attr_reader   :protocol_binding
      attr_accessor :attributes_index
      attr_accessor :force_authn
      attr_accessor :certificate
      attr_accessor :certificate_new
      attr_accessor :private_key
      attr_accessor :authn_context
      attr_accessor :authn_context_comparison
      attr_accessor :authn_context_decl_ref
      attr_reader :attribute_consuming_service
      # Work-flow
      attr_accessor :security
      attr_accessor :soft
      # Deprecated
      attr_accessor :assertion_consumer_logout_service_url
      attr_reader   :assertion_consumer_logout_service_binding
      attr_accessor :issuer
      attr_accessor :idp_sso_target_url
      attr_accessor :idp_slo_target_url

      # @return [String] IdP Single Sign On Service URL
      #
      def idp_sso_service_url
        @idp_sso_service_url || @idp_sso_target_url
      end

      # @return [String] IdP Single Logout Service URL
      #
      def idp_slo_service_url
        @idp_slo_service_url || @idp_slo_target_url
      end

      # @return [String] IdP Single Sign On Service Binding
      #
      def idp_sso_service_binding
        @idp_sso_service_binding || idp_binding_from_embed_sign
      end

      # Setter for IdP Single Sign On Service Binding
      # @param value [String, Symbol].
      #
      def idp_sso_service_binding=(value)
        @idp_sso_service_binding = get_binding(value)
      end

      # @return [String] IdP Single Logout Service Binding
      #
      def idp_slo_service_binding
        @idp_slo_service_binding || idp_binding_from_embed_sign
      end

      # Setter for IdP Single Logout Service Binding
      # @param value [String, Symbol].
      #
      def idp_slo_service_binding=(value)
        @idp_slo_service_binding = get_binding(value)
      end

      # @return [String] SP Entity ID
      #
      def sp_entity_id
        @sp_entity_id || @issuer
      end

      # Setter for SP Protocol Binding
      # @param value [String, Symbol].
      #
      def protocol_binding=(value)
        @protocol_binding = get_binding(value)
      end

      # Setter for SP Assertion Consumer Service Binding
      # @param value [String, Symbol].
      #
      def assertion_consumer_service_binding=(value)
        @assertion_consumer_service_binding = get_binding(value)
      end

      # @return [String] Single Logout Service URL.
      #
      def single_logout_service_url
        @single_logout_service_url || @assertion_consumer_logout_service_url
      end

      # @return [String] Single Logout Service Binding.
      #
      def single_logout_service_binding
        @single_logout_service_binding || @assertion_consumer_logout_service_binding
      end

      # Setter for Single Logout Service Binding.
      #
      # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect")
      # @param value [String, Symbol]
      #
      def single_logout_service_binding=(value)
        @single_logout_service_binding = get_binding(value)
      end

      # @deprecated Setter for legacy Single Logout Service Binding parameter.
      #
      # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect")
      # @param value [String, Symbol]
      #
      def assertion_consumer_logout_service_binding=(value)
        @assertion_consumer_logout_service_binding = get_binding(value)
      end

      # Calculates the fingerprint of the IdP x509 certificate.
      # @return [String] The fingerprint
      #
      def get_fingerprint
        idp_cert_fingerprint || begin
          idp_cert = get_idp_cert
          if idp_cert
            fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(idp_cert_fingerprint_algorithm).new
            fingerprint_alg.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":")
          end
        end
      end

      # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it)
      #
      def get_idp_cert
        return nil if idp_cert.nil? || idp_cert.empty?

        formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
        OpenSSL::X509::Certificate.new(formatted_cert)
      end

      # @return [Hash with 2 arrays of OpenSSL::X509::Certificate] Build multiple IdP certificates from the settings.
      #
      def get_idp_cert_multi
        return nil if idp_cert_multi.nil? || idp_cert_multi.empty?

        raise ArgumentError.new("Invalid value for idp_cert_multi") if not idp_cert_multi.is_a?(Hash)

        certs = {:signing => [], :encryption => [] }

        if idp_cert_multi.key?(:signing) and not idp_cert_multi[:signing].empty?
          idp_cert_multi[:signing].each do |idp_cert|
            formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
            certs[:signing].push(OpenSSL::X509::Certificate.new(formatted_cert))
          end
        end

        if idp_cert_multi.key?(:encryption) and not idp_cert_multi[:encryption].empty?
          idp_cert_multi[:encryption].each do |idp_cert|
            formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
            certs[:encryption].push(OpenSSL::X509::Certificate.new(formatted_cert))
          end
        end

        certs
      end

      # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
      #
      def get_sp_cert
        return nil if certificate.nil? || certificate.empty?

        formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
        cert = OpenSSL::X509::Certificate.new(formatted_cert)

        if security[:check_sp_cert_expiration]
          if OneLogin::RubySaml::Utils.is_cert_expired(cert)
            raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.")
          end
        end

        cert
      end

      # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it)
      #
      def get_sp_cert_new
        return nil if certificate_new.nil? || certificate_new.empty?

        formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate_new)
        OpenSSL::X509::Certificate.new(formatted_cert)
      end

      # @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it)
      #
      def get_sp_key
        return nil if private_key.nil? || private_key.empty?

        formatted_private_key = OneLogin::RubySaml::Utils.format_private_key(private_key)
        OpenSSL::PKey::RSA.new(formatted_private_key)
      end

      private

      def idp_binding_from_embed_sign
        security[:embed_sign] ? Utils::BINDINGS[:post] : Utils::BINDINGS[:redirect]
      end

      def get_binding(value)
        return unless value

        Utils::BINDINGS[value.to_sym] || value
      end

      DEFAULTS = {
        :assertion_consumer_service_binding        => Utils::BINDINGS[:post],
        :single_logout_service_binding             => Utils::BINDINGS[:redirect],
        :idp_cert_fingerprint_algorithm            => XMLSecurity::Document::SHA1,
        :compress_request                          => true,
        :compress_response                         => true,
        :message_max_bytesize                      => 250000,
        :soft                                      => true,
        :double_quote_xml_attribute_values         => false,
        :security                                  => {
          :authn_requests_signed      => false,
          :logout_requests_signed     => false,
          :logout_responses_signed    => false,
          :want_assertions_signed     => false,
          :want_assertions_encrypted  => false,
          :want_name_id               => false,
          :metadata_signed            => false,
          :embed_sign                 => false, # Deprecated
          :digest_method              => XMLSecurity::Document::SHA1,
          :signature_method           => XMLSecurity::Document::RSA_SHA1,
          :check_idp_cert_expiration  => false,
          :check_sp_cert_expiration   => false
        }.freeze
      }.freeze
    end
  end
end
