# frozen_string_literal: true

#-------------------------------------------------------------------------
# # Copyright (c) Microsoft and contributors. All rights reserved.
#
# The MIT License(MIT)

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :

# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#--------------------------------------------------------------------------

require "azure/storage/common/core"
require "azure/storage/common/client_options_error"
require "azure/core/auth/signer"
require "time"
require "uri"

# @see https://msdn.microsoft.com/library/azure/dn140255.aspx for more information on construction
module Azure::Storage::Common::Core
  module Auth
    class SharedAccessSignature
      DEFAULTS = {
        permissions: "r",
        version: Azure::Storage::Common::Default::STG_VERSION
      }

      SERVICE_TYPE_MAPPING = {
        b: Azure::Storage::Common::ServiceType::BLOB,
        t: Azure::Storage::Common::ServiceType::TABLE,
        q: Azure::Storage::Common::ServiceType::QUEUE,
        f: Azure::Storage::Common::ServiceType::FILE
      }

      ACCOUNT_KEY_MAPPINGS = {
        version:              :sv,
        service:              :ss,
        resource:             :srt,
        permissions:          :sp,
        start:                :st,
        expiry:               :se,
        protocol:             :spr,
        ip_range:             :sip
      }

      SERVICE_KEY_MAPPINGS = {
        version:              :sv,
        permissions:          :sp,
        start:                :st,
        expiry:               :se,
        identifier:           :si,
        protocol:             :spr,
        ip_range:             :sip
      }

      USER_DELEGATION_KEY_MAPPINGS = {
        signed_oid:           :skoid,
        signed_tid:           :sktid,
        signed_start:         :skt,
        signed_expiry:        :ske,
        signed_service:       :sks,
        signed_version:       :skv
      }

      BLOB_KEY_MAPPINGS = {
        resource:             :sr,
        timestamp:            :snapshot,
        cache_control:        :rscc,
        content_disposition:  :rscd,
        content_encoding:     :rsce,
        content_language:     :rscl,
        content_type:         :rsct
      }

      TABLE_KEY_MAPPINGS = {
        table_name:           :tn,
        startpk:              :spk,
        endpk:                :epk,
        startrk:              :srk,
        endrk:                :erk
      }

      FILE_KEY_MAPPINGS = {
        resource:             :sr,
        cache_control:        :rscc,
        content_disposition:  :rscd,
        content_encoding:     :rsce,
        content_language:     :rscl,
        content_type:         :rsct
      }

      SERVICE_OPTIONAL_QUERY_PARAMS = [:sp, :si, :sip, :spr, :rscc, :rscd, :rsce, :rscl, :rsct, :spk, :srk, :epk, :erk]

      ACCOUNT_OPTIONAL_QUERY_PARAMS = [:st, :sip, :spr]

      attr :account_name

      # Public: Initialize the SharedAccessSignature generator
      #
      # @param account_name [String] The account name. Defaults to the one in the global configuration.
      # @param access_key [String]   The access_key encoded in Base64. Defaults to the one in the global configuration.
      # @param user_delegation_key [Azure::Storage::Common::UserDelegationKey] The user delegation key obtained from
      # calling get_user_delegation_key after authenticating with an Azure Active Directory entity. If present, the
      # SAS is signed with the user delegation key instead of the access key.
      def initialize(account_name = "", access_key = "", user_delegation_key = nil)
        if access_key.empty? && !user_delegation_key.nil?
          access_key = user_delegation_key.value
        end
        if account_name.empty? || access_key.empty?
          client = Azure::Storage::Common::Client.create_from_env
          account_name = client.storage_account_name if account_name.empty?
          access_key = client.storage_access_key if access_key.empty?
        end
        @account_name = account_name
        @user_delegation_key = user_delegation_key
        @signer = Azure::Core::Auth::Signer.new(access_key)
      end

      # Service Shared Access Signature Token for the given path and options
      # @param path     [String] Path of the URI or the table name
      # @param options  [Hash]
      #
      # ==== Options
      #
      # * +:service+             - String. Required. Service type. 'b' (blob) or 'q' (queue) or 't' (table) or 'f' (file).
      # * +:resource+            - String. Required. Resource type, 'b' (blob) or 'c' (container) or 'f' (file) or 's' (share).
      # * +:permissions+         - String. Optional. Combination of 'r', 'a', 'c', w','d','l' in this order for a container.
      #                                              Combination of 'r', 'a', 'c', 'w', 'd' in this order for a blob.
      #                                              Combination of 'r', 'c', 'w', 'd', 'l' in this order for a share.
      #                                              Combination of 'r', 'c', 'w', 'd' in this order for a file.
      #                                              Combination of 'r', 'a', 'u', 'p' in this order for a queue.
      #                                              Combination of 'r', 'a', 'u', 'd' in this order for a table.
      #                                              This option must be omitted if it has been specified in an associated stored access policy.
      # * +:start+               - String. Optional. UTC Date/Time in ISO8601 format.
      # * +:expiry+              - String. Optional. UTC Date/Time in ISO8601 format. Default now + 30 minutes.
      # * +:identifier+          - String. Optional. Identifier for stored access policy.
      #                                              This option must be omitted if a user delegation key has been provided.
      # * +:protocol+            - String. Optional. Permitted protocols.
      # * +:ip_range+            - String. Optional. An IP address or a range of IP addresses from which to accept requests.
      #
      # Below options for blob serivce only
      # * +:snapshot+            - String. Optional. UTC Date/Time in ISO8601 format. The blob snapshot to grant permission.
      # * +:cache_control+       - String. Optional. Response header override.
      # * +:content_disposition+ - String. Optional. Response header override.
      # * +:content_encoding+    - String. Optional. Response header override.
      # * +:content_language+    - String. Optional. Response header override.
      # * +:content_type+        - String. Optional. Response header override.
      #
      # Below options for table service only
      # * +:startpk+             - String. Optional but must accompany startrk. The start partition key of a specified partition key range.
      # * +:endpk+               - String. Optional but must accompany endrk. The end partition key of a specified partition key range.
      # * +:startrk+             - String. Optional. The start row key of a specified row key range.
      # * +:endrk+               - String. Optional. The end row key of a specified row key range.
      def generate_service_sas_token(path, options = {})
        if options.key?(:service)
          service_type = SERVICE_TYPE_MAPPING[options[:service].to_sym]
          options.delete(:service)
        end

        raise Azure::Storage::Common::InvalidOptionsError, "SAS version cannot be set" if options[:version]

        options = DEFAULTS.merge(options)
        valid_mappings = SERVICE_KEY_MAPPINGS
        if service_type == Azure::Storage::Common::ServiceType::BLOB
          if options[:resource]
            options.merge!(resource: options[:resource])
          else
            options.merge!(resource: "b")
          end
          valid_mappings.merge!(BLOB_KEY_MAPPINGS)
        elsif service_type == Azure::Storage::Common::ServiceType::TABLE
          options.merge!(table_name: path)
          valid_mappings.merge!(TABLE_KEY_MAPPINGS)
        elsif service_type == Azure::Storage::Common::ServiceType::FILE
          if options[:resource]
            options.merge!(resource: options[:resource])
          else
            options.merge!(resource: "f")
          end
          valid_mappings.merge!(FILE_KEY_MAPPINGS)
        end

        service_key_mappings = SERVICE_KEY_MAPPINGS
        unless @user_delegation_key.nil?
          valid_mappings.delete(:identifier)
          USER_DELEGATION_KEY_MAPPINGS.each { |k, _| options[k] = @user_delegation_key.send(k) }
          valid_mappings.merge!(USER_DELEGATION_KEY_MAPPINGS)
          service_key_mappings = service_key_mappings.merge(USER_DELEGATION_KEY_MAPPINGS)
        end

        invalid_options = options.reject { |k, _| valid_mappings.key?(k) }
        raise Azure::Storage::Common::InvalidOptionsError, "invalid options #{invalid_options} provided for SAS token generate" if invalid_options.length > 0

        canonicalize_time(options)

        query_hash = Hash[options.map { |k, v| [service_key_mappings[k], v] }]
        .reject { |k, v| SERVICE_OPTIONAL_QUERY_PARAMS.include?(k) && v.to_s == "" }
        .merge(sig: @signer.sign(signable_string_for_service(service_type, path, options)))

        URI.encode_www_form(query_hash)
      end

      # Construct the plaintext to the spec required for signatures
      # See https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas
      # @return [String]
      def signable_string_for_service(service_type, path, options)
        # Order is significant
        # The newlines from empty strings here are required
        signable_fields =
        [
          options[:permissions],
          options[:start],
          options[:expiry],
          canonicalized_resource(service_type, path)
        ]

        if @user_delegation_key.nil?
          signable_fields.push(options[:identifier])
        else
          signable_fields.concat [
            @user_delegation_key.signed_oid,
            @user_delegation_key.signed_tid,
            @user_delegation_key.signed_start,
            @user_delegation_key.signed_expiry,
            @user_delegation_key.signed_service,
            @user_delegation_key.signed_version,
            # User delegation fields (supported since 2020-02-10)
            options[:signed_authorized_user_object_id] || "",    # saoid
            options[:signed_unauthorized_user_object_id] || "",  # suoid
            options[:signed_correlation_id] || "",               # scid
            # For 2025-07-05 USER DELEGATION SAS ONLY, add two empty lines after correlation ID
            # This is the critical difference that was causing the signature mismatch with 2018-11-09.
            "",
            ""
          ]

        end

        signable_fields.concat [
          options[:ip_range],
          options[:protocol],
          Azure::Storage::Common::Default::STG_VERSION
        ]

        signable_fields.concat [
          options[:resource],
          options[:timestamp] || "",
          options[:signed_encryption_scope] || ""
        ] if service_type == Azure::Storage::Common::ServiceType::BLOB

        signable_fields.concat [
          options[:cache_control],
          options[:content_disposition],
          options[:content_encoding],
          options[:content_language],
          options[:content_type]
        ] if service_type == Azure::Storage::Common::ServiceType::BLOB || service_type == Azure::Storage::Common::ServiceType::FILE

        signable_fields.concat [
          options[:startpk],
          options[:startrk],
          options[:endpk],
          options[:endrk]
        ] if service_type == Azure::Storage::Common::ServiceType::TABLE

        signable_fields.join "\n"
      end

      # Account Shared Access Signature Token for the given options
      # @param account_name     [String] storage account name
      # @param options          [Hash]
      #
      # ==== Options
      #
      # * +:service+             - String. Required. Accessible services. Combination of 'b' (blob), 'q' (queue), 't' (table), 'f' (file).
      # * +:resource+            - String. Required. Accessible resource types. Combination of 's' (service), 'c' (container-level), 'o'(object-level).
      # * +:permissions+         - String. Required. Permissions. Combination of 'r' (read), 'w' (write), 'd'(delete), 'l'(list), 'a'(add),
      #                                              'c'(create), 'u'(update), 'p'(process). Permissions are only valid if they match
      #                                              the specified signed resource type; otherwise they are ignored.
      # * +:start+               - String. Optional. UTC Date/Time in ISO8601 format.
      # * +:expiry+              - String. Optional. UTC Date/Time in ISO8601 format. Default now + 30 minutes.
      # * +:protocol+            - String. Optional. Permitted protocols.
      # * +:ip_range+            - String. Optional. An IP address or a range of IP addresses from which to accept requests.
      #                                    When specifying a range, note that the range is inclusive.
      def generate_account_sas_token(options = {})
        raise Azure::Storage::Common::InvalidOptionsError, "SAS version cannot be set" if options[:version]

        options = DEFAULTS.merge(options)
        valid_mappings = ACCOUNT_KEY_MAPPINGS

        invalid_options = options.reject { |k, _| valid_mappings.key?(k) }
        raise Azure::Storage::Common::InvalidOptionsError, "invalid options #{invalid_options} provided for SAS token generate" if invalid_options.length > 0

        canonicalize_time(options)

        query_hash = Hash[options.map { |k, v| [ACCOUNT_KEY_MAPPINGS[k], v] }]
        .reject { |k, v| ACCOUNT_OPTIONAL_QUERY_PARAMS.include?(k) && v.to_s == "" }
        .merge(sig: @signer.sign(signable_string_for_account(options)))

        URI.encode_www_form(query_hash)
      end

      # Construct the plaintext to the spec required for signatures
      # @return [String]
      def signable_string_for_account(options)
        # Order is significant
        # The newlines from empty strings here are required
        [
          @account_name,
          options[:permissions],
          options[:service],
          options[:resource],
          options[:start],
          options[:expiry],
          options[:ip_range],
          options[:protocol],
          Azure::Storage::Common::Default::STG_VERSION,
          ""
        ].join("\n")
      end

      # Return the cononicalized resource representation of the blob resource
      # @return [String]
      def canonicalized_resource(service_type, path)
        "/#{service_type}/#{account_name}#{path.start_with?('/') ? '' : '/'}#{path}"
      end

      def canonicalize_time(options)
        options[:start] = Time.parse(options[:start]).utc.iso8601 if options[:start]
        options[:expiry] = Time.parse(options[:expiry]).utc.iso8601 if options[:expiry]
        options[:expiry] ||= (Time.now + 60 * 30).utc.iso8601
      end

      # A customised URI reflecting options for the resource signed with Shared Access Signature
      # @param uri                [URI] uri to resource including query options
      # @param use_account_sas    [Boolean] Whether uses account SAS
      # @param options            [Hash]
      #
      # ==== Options
      #
      # * +:start+                - String. Optional. UTC Date/Time in ISO8601 format.
      # * +:expiry+               - String. Optional. UTC Date/Time in ISO8601 format. Default now + 30 minutes.
      # * +:protocol+             - String. Optional. Permitted protocols.
      # * +:ip_range+             - String. Optional. An IP address or a range of IP addresses from which to accept requests.
      #                                     When specifying a range, note that the range is inclusive.
      #
      # Below options for account SAS only
      # * +:service+              - String. Required. Accessible services. Combination of 'b' (blob), 'q' (queue), 't' (table), 'f' (file).
      # * +:resource+             - String. Required. Accessible resource types. Combination of 's' (service), 'c' (container-level), 'o'(object-level).
      # * +:permissions+          - String. Required. Permissions. Combination of 'r' (read), 'w' (write), 'd'(delete), 'l'(list), 'a'(add),
      #                                               'c'(create), 'u'(update), 'p'(process). Permissions are only valid if they match
      #                                               the specified signed resource type; otherwise they are ignored.
      #
      # Below options for service SAS only
      # * +:service+              - String. Required. Service type. 'b' (blob) or 'q' (queue) or 't' (table) or 'f' (file).
      # * +:resource+             - String. Required. Resource type, 'b' (blob) or 'c' (container) or 'f' (file) or 's' (share).
      # * +:identifier+           - String. Optional. Identifier for stored access policy.
      # * +:permissions+          - String. Optional. Combination of 'r', 'a', 'c', w','d','l' in this order for a container.
      #                                               Combination of 'r', 'a', 'c', 'w', 'd' in this order for a blob.
      #                                               Combination of 'r', 'c', 'w', 'd', 'l' in this order for a share.
      #                                               Combination of 'r', 'c', 'w', 'd' in this order for a file.
      #                                               Combination of 'r', 'a', 'u', 'p' in this order for a queue.
      #                                               Combination of 'r', 'a', 'u', 'd' in this order for a table.
      #
      # Below options for Blob service only
      # * +:cache_control+        - String. Optional. Response header override.
      # * +:content_disposition+  - String. Optional. Response header override.
      # * +:content_encoding+     - String. Optional. Response header override.
      # * +:content_language+     - String. Optional. Response header override.
      # * +:content_type+         - String. Optional. Response header override.
      #
      # Below options for Table service only
      # * +:table_name+           - String. Required. Table name for SAS.
      # * +:startpk+              - String. Optional but must accompany startrk. The start partition key of a specified partition key range.
      # * +:endpk+                - String. Optional but must accompany endrk. The end partition key of a specified partition key range.
      # * +:startrk+              - String. Optional. The start row key of a specified row key range.
      # * +:endrk+                - String. Optional. The end row key of a specified row key range.
      def signed_uri(uri, use_account_sas, options)
        CGI::parse(uri.query || "").inject({}) { |memo, (k, v)| memo[k.to_sym] = v; memo }

        if options[:service] == (nil) && uri.host != (nil)
          host_splits = uri.host.split(".")
          options[:service] = host_splits[1].chr if host_splits.length > 1 && host_splits[0] == @account_name
        end

        sas_params = if use_account_sas
          generate_account_sas_token(options)
                     else
                       generate_service_sas_token(uri.path, options)
                     end

        URI.parse(uri.to_s + (uri.query.nil? ? "?" : "&") + sas_params)
      end
    end
  end
end
