#-------------------------------------------------------------------------
# # Copyright (c) Microsoft and contributors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#--------------------------------------------------------------------------

module Azure
  # The Azure::Configurable module provides basic configuration for Azure activities.
  module Configurable

    # @!attribute [w] storage_access_key
    #   @return [String] Azure Storage access key.
    # @!attribute storage_account_name
    #   @return [String] Azure Storage account name
    # @!attribute sb_access_key
    #   @return [String] Azure Service Bus access key
    # @!attribute sb_sas_key
    #   @return [String] Azure Service Bus Shared Access Signature key
    # @!attribute sb_sas_key_name
    #   @return [String] Azure Service Bus Shared Access Signature key name
    # @!attribute sb_namespace
    #   @return [String] Azure Service Bus namespace
    # @!attribute sb_issuer
    #   @return [String] Azure Service Bus issuer
    # @!attribute ca_file
    #   @return [String] Location of the Certificate Authority bundle to be used for HTTPS
    # @!attribute management_certificate
    #   @return [String|File] Azure Service Management certificate (pfx or pem)
    # @!attribute subscription_id
    #   @return [String] Azure Subscription ID
    # @!attribute http_private_key
    #   @return [String] Private key used for HTTPS certificate based authentication (derived from the management cert)
    # @!attribute http_certificate_key
    #   @return [String] Public key used for HTTPS certificate based authentication (derived from the management cert)
    # @!attribute sql_database_management_endpoint
    #   @return [String] Azure SQL database management endpoint. default: https://management.core.windows.net:8443/
    # @!attribute storage_blob_host
    #   @return [String] Set the host for the Blob service. Only set this if you want
    #     something custom (like, for example, to point this to a LocalStorage
    #     emulator). This should be the complete host, including http:// at the
    #     start. When using the emulator, make sure to include your account name at
    #     the end.
    # @!attribute storage_table_host
    #   @return [String] Get the host for this service. If you set something using #storage_table_host=,
    #     then we use that. Else we default to Azure's default hosts, based
    #     on your account name.
    # @!attribute storage_queue_host
    #   @return [String] Set the host for the Queue service. Only set this if you want
    #     something custom (like, for example, to point this to a LocalStorage
    #     emulator). This should be the complete host, including http:// at the
    #     start. When using the emulator, make sure to include your account name at
    #     the end.
    # @!attribute management_endpoint
    #   @return [String] Azure Service Management Endpoint. default: https://management.core.windows.net

    attr_accessor :storage_access_key,
                  :storage_account_name,
                  :sb_access_key,
                  :sb_namespace,
                  :sb_sas_key,
                  :sb_sas_key_name,
                  :sb_issuer,
                  :ca_file,
                  :subscription_id

    attr_reader :http_private_key,
                :http_certificate_key,
                :acs_host,
                :service_bus_host

    attr_writer :storage_table_host,
                :storage_blob_host,
                :storage_queue_host,
                :sql_database_management_endpoint,
                :management_endpoint

    class << self

      # List of configurable keys for {Azure::Client}
      # @return [Array] of option keys
      def keys
        @keys ||= [
            :storage_access_key,
            :storage_account_name,
            :sb_access_key,
            :sb_sas_key,
            :sb_sas_key_name,
            :ca_file,
            :sb_namespace,
            :management_certificate,
            :subscription_id,
            :sql_database_management_endpoint,
            :sb_issuer,
            :storage_table_host,
            :storage_blob_host,
            :storage_queue_host,
            :management_endpoint
        ]
      end
    end

    # Set configuration options using a block
    def configure
      yield self
    end

    # Reset configuration options to default values
    def reset!(options = {})
      Azure::Configurable.keys.each do |key|
        value = if self == Azure
                  Azure::Default.options[key]
                else
                  Azure.send(key)
                end

        if key == :management_certificate
          @certificate_key = nil
          @private_key = nil
          send(:"#{key.to_s + '='}", value)
        else
          instance_variable_set(:"@#{key}", options.fetch(key, value))
        end
      end
      self.send(:reset_agents!) if self.respond_to?(:reset_agents!)
      self
    end

    alias setup reset!

    def management_certificate
      @management_certificate
    end

    # Set the management certificate via string or file (populates #http_private_key and #http_certificate_key)
    # @param  [String|File] the string or file representing the .pem or .pfx
    def management_certificate=(cert_string_or_file)
      self.send(:reset_agents!) if self.respond_to?(:reset_agents!)
      if cert_string_or_file.nil?
        @certificate_key = @private_key = @management_certificate = nil
      else

        # the pfx may have null chars which will raise an exception in File.file?
        invalid_file_chars = cert_string_or_file.to_s =~ /\x00/

        # validate only if input is file path
        if !invalid_file_chars && File.file?(cert_string_or_file) && File.extname(cert_string_or_file).downcase =~ /(pem|pfx)$/
          error_message = "Could not read from file '#{cert_string_or_file}'."
          raise ArgumentError.new(error_message) unless test('r', cert_string_or_file)
        end

        # get the string representation of cert
        cert_file = if !invalid_file_chars && File.file?(cert_string_or_file)
                      read_cert_from_file(cert_string_or_file)
                    else
                      cert_string_or_file
                    end

        begin
          if cert_file =~ /-----BEGIN CERTIFICATE-----/
            # Parse pem content
            @certificate_key = OpenSSL::X509::Certificate.new(cert_file)
            @private_key = OpenSSL::PKey::RSA.new(cert_file)
          else
            # Parse pfx content
            cert_content = OpenSSL::PKCS12.new(cert_file)
            @certificate_key = OpenSSL::X509::Certificate.new(
                cert_content.certificate.to_pem
            )
            @private_key = OpenSSL::PKey::RSA.new(cert_content.key.to_pem)
          end
          @management_certificate = cert_file
        rescue OpenSSL::OpenSSLError => e
          @certificate_key = nil
          @private_key = nil
          raise ArgumentError.new("Management certificate not valid. Error: #{e.message}")
        end
      end
    end

    # Private side of the management certificate
    # @returns [OpenSSL::X509::Certificate]
    def http_private_key
      @private_key
    end

    # Public side of the management certificate
    # @returns [OpenSSL::PKey::RSA]
    def http_certificate_key
      @certificate_key
    end

    def management_endpoint
      normalize_endpoint do
        if URI(@management_endpoint).scheme.nil?
          "https://#{@management_endpoint}"
        else
          @management_endpoint
        end
      end
    end

    def sql_database_management_endpoint
      normalize_endpoint do
        if URI(@sql_database_management_endpoint).scheme.nil?
          "https://#{@sql_database_management_endpoint}:8443"
        else
          @sql_database_management_endpoint
        end
      end
    end

    # Storage queue host
    # @return [String]
    def storage_queue_host
      @storage_queue_host || default_host(:queue)
    end

    # Storage blob host
    # @return [String]
    def storage_blob_host
      @storage_blob_host || default_host(:blob)
    end

    # Storage table host
    # @return [String]
    def storage_table_host
      @storage_table_host || default_host(:table)
    end

    # Get the host for the ACS service.
    # @return [String]
    def acs_host
      if sb_namespace.empty?
        nil
      else
        "https://#{sb_namespace}-sb.accesscontrol.windows.net"
      end
    end

    # Get the host for Service Bus based on the Service Bus Namespace.
    # @return [String]
    def service_bus_host
      if sb_namespace.empty?
        nil
      else
        "https://#{sb_namespace}.servicebus.windows.net"
      end
    end

    def config
      self
    end

    private

    def default_host(service)
      "https://#{storage_account_name}.#{service}.core.windows.net"
    end

    def read_cert_from_file(cert_file_path)
      if File.extname(cert_file_path).downcase == '.pem'
        File.read(cert_file_path)
      elsif File.extname(cert_file_path).downcase == '.publishsettings'
        management_cert = parse_publishsettings(cert_file_path)['ManagementCertificate']
        Base64.decode64(management_cert)
      else
        File.binread(cert_file_path)
      end
    end

    def parse_publishsettings(pub_file_path)
      pub_xml = Nokogiri::XML.parse(File.read(pub_file_path))
      pub_xml.css('PublishData PublishProfile Subscription')[0]
    end

    def normalize_endpoint
      if block_given?
        File.join(yield, '')
      else
        nil
      end
    end

    def options
      Hash[Azure::Configurable.keys.map { |key| [key, instance_variable_get(:"@#{key}")] }]
    end

  end
end