# 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 "nokogiri"
require "time"

require "azure/storage/common/service/enumeration_results"
require "azure/storage/common/service/signed_identifier"
require "azure/storage/common/service/access_policy"
require "azure/storage/common/service/storage_service_properties"
require "azure/storage/common/service/logging"
require "azure/storage/common/service/metrics"
require "azure/storage/common/service/retention_policy"
require "azure/storage/common/service/cors"
require "azure/storage/common/service/cors_rule"
require "azure/storage/common/service/storage_service_stats"

module Azure::Storage::Common
  module Service
    module Serialization
      module ClassMethods
        def signed_identifiers_from_xml(xml)
          xml = slopify(xml)
          expect_node("SignedIdentifiers", xml)

          identifiers = []
          return identifiers unless (xml > "SignedIdentifier").any?

          if xml.SignedIdentifier.count == 0
            identifiers.push(signed_identifier_from_xml(xml.SignedIdentifier))
          else
            xml.SignedIdentifier.each { |identifier_node|
              identifiers.push(signed_identifier_from_xml(identifier_node))
            }
          end

          identifiers
        end

        def signed_identifiers_to_xml(signed_identifiers)
          builder = Nokogiri::XML::Builder.new(encoding: "utf-8") do |xml|
            xml.SignedIdentifiers {
              signed_identifiers.each do |identifier|
                xml.SignedIdentifier {
                  xml.Id identifier.id
                  xml.AccessPolicy {
                    xml.Start identifier.access_policy.start
                    xml.Expiry identifier.access_policy.expiry
                    xml.Permission identifier.access_policy.permission
                  }
                }
              end
            }
          end
          builder.to_xml
        end

        def signed_identifier_from_xml(xml)
          xml = slopify(xml)
          expect_node("SignedIdentifier", xml)

          SignedIdentifier.new do |identifier|
            identifier.id = xml.Id.text if (xml > "Id").any?
            identifier.access_policy = access_policy_from_xml(xml.AccessPolicy) if (xml > "AccessPolicy").any?
          end
        end

        def access_policy_from_xml(xml)
          xml = slopify(xml)
          expect_node("AccessPolicy", xml)

          AccessPolicy.new do |policy|
            policy.start = xml.Start.text if (xml > "Start").any?
            policy.expiry = xml.Expiry.text if (xml > "Expiry").any?
            policy.permission = xml.Permission.text if (xml > "Permission").any?
          end
        end

        def enumeration_results_from_xml(xml, results)
          xml = slopify(xml)
          expect_node("EnumerationResults", xml)

          results = results || Service::EnumerationResults.new;

          results.continuation_token = xml.NextMarker.text if (xml > "NextMarker").any?
          results
        end

        def metadata_from_xml(xml)
          xml = slopify(xml)
          expect_node("Metadata", xml)

          metadata = {}

          xml.children.each { |meta_node|

            key = meta_node.name.downcase
            if metadata.has_key? key
              metadata[key] = [metadata[key]] unless metadata[key].respond_to? :push
              metadata[key].push(meta_node.text)
            else
              metadata[key] = meta_node.text
            end
          }
          metadata
        end

        def metadata_from_headers(headers)
          metadata = {}

          headers.each { |k, v|
            if key = k[/^x-ms-meta-(.*)/, 1]
              if metadata.has_key? key
                metadata[key] = [metadata[key]] unless metadata[key].respond_to? :push
                metadata[key].push(v)
              else
                metadata[key] = v
              end
            end
          }

          metadata
        end

        def retention_policy_to_xml(retention_policy, xml)
          xml.RetentionPolicy {
            xml.Enabled retention_policy.enabled
            xml.Days retention_policy.days if retention_policy.enabled && retention_policy.days
          } if retention_policy
        end

        def retention_policy_from_xml(xml)
          xml = slopify(xml)
          expect_node("RetentionPolicy", xml)

          RetentionPolicy.new do |policy|
            policy.enabled = to_bool(xml.Enabled.text) if (xml > "Enabled").any?
            policy.days = xml.Days.text.to_i if (xml > "Days").any?
          end
        end

        def metrics_to_xml_children(metrics, xml)
          return unless metrics
          xml.Version metrics.version
          xml.Enabled metrics.enabled
          xml.IncludeAPIs metrics.include_apis if metrics.enabled
          retention_policy_to_xml(metrics.retention_policy, xml) if metrics.retention_policy
        end

        def hour_metrics_to_xml(metrics, xml)
          xml.HourMetrics {
            metrics_to_xml_children(metrics, xml)
          } if metrics
        end

        def minute_metrics_to_xml(metrics, xml)
          xml.MinuteMetrics {
            metrics_to_xml_children(metrics, xml)
          } if metrics
        end

        def metrics_from_xml(xml)
          xml = slopify(xml)

          Metrics.new do |metrics|
            metrics.version = xml.Version.text if (xml > "Version").any?
            metrics.enabled = to_bool(xml.Enabled.text) if (xml > "Enabled").any?
            metrics.include_apis = to_bool(xml.IncludeAPIs.text) if (xml > "IncludeAPIs").any?
            metrics.retention_policy = retention_policy_from_xml(xml.RetentionPolicy)
          end
        end

        def logging_to_xml(logging, xml)
          xml.Logging {
            xml.Version logging.version
            xml.Delete logging.delete
            xml.Read logging.read
            xml.Write logging.write
            retention_policy_to_xml(logging.retention_policy, xml)
          } if logging
        end

        def logging_from_xml(xml)
          xml = slopify(xml)
          expect_node("Logging", xml)

          Logging.new do |logging|
            logging.version = xml.Version.text if (xml > "Version").any?
            logging.delete = to_bool(xml.Delete.text) if (xml > "Delete").any?
            logging.read = to_bool(xml.Read.text) if (xml > "Read").any?
            logging.write = to_bool(xml.Write.text) if (xml > "Write").any?
            logging.retention_policy = retention_policy_from_xml(xml.RetentionPolicy)
          end
        end

        def cors_to_xml(cors, xml)
          xml.Cors {
            cors.cors_rules.to_a.each do |cors_rule|
              cors_rule_to_xml(cors_rule, xml)
            end
          }
        end

        def cors_rule_to_xml(cors_rule, xml)
          xml.CorsRule {
            xml.AllowedOrigins cors_rule.allowed_origins.join(",") if cors_rule.allowed_origins
            xml.AllowedMethods cors_rule.allowed_methods.join(",") if cors_rule.allowed_methods
            xml.MaxAgeInSeconds cors_rule.max_age_in_seconds if cors_rule.max_age_in_seconds
            xml.ExposedHeaders cors_rule.exposed_headers.join(",") if cors_rule.exposed_headers
            xml.AllowedHeaders cors_rule.allowed_headers.join(",") if cors_rule.allowed_headers
          }
        end

        def cors_from_xml(xml)
          xml = slopify(xml)
          expect_node("Cors", xml)

          Cors.new do |cors|
            cors.cors_rules = xml.children.to_a.map { |child| cors_rule_from_xml(child) }
          end
        end

        def cors_rule_from_xml(xml)
          xml = slopify(xml)
          expect_node("CorsRule", xml)

          CorsRule.new do |cors_rule|
            cors_rule.allowed_origins =  ary_from_node(xml.AllowedOrigins) if (xml > "AllowedOrigins").any?
            cors_rule.allowed_methods =  ary_from_node(xml.AllowedMethods) if (xml > "AllowedMethods").any?
            cors_rule.max_age_in_seconds = xml.MaxAgeInSeconds.text.to_i if (xml > "MaxAgeInSeconds").any?
            cors_rule.exposed_headers = ary_from_node(xml.ExposedHeaders) if (xml > "ExposedHeaders").any?
            cors_rule.allowed_headers = ary_from_node(xml.AllowedHeaders) if (xml > "AllowedHeaders").any?
          end
        end

        def geo_replication_from_xml(xml)
          xml = slopify(xml)
          expect_node("GeoReplication", xml)

          GeoReplication.new do |geo_replication|
            geo_replication.status = xml.Status.text if (xml > "Status").any?
            geo_replication.last_sync_time =
              begin
                Time.parse(xml.LastSyncTime.text) if (xml > "LastSyncTime").any?
              rescue
                nil
              end
          end
        end

        def ary_from_node(node)
          node.text.split(",").map { |s| s.strip }
        end

        def service_properties_to_xml(properties)
          builder = Nokogiri::XML::Builder.new(encoding: "utf-8") do |xml|
            xml.StorageServiceProperties {
              xml.DefaultServiceVersion(properties.default_service_version) if properties.default_service_version
              logging_to_xml(properties.logging, xml) if properties.logging
              hour_metrics_to_xml(properties.hour_metrics, xml) if properties.hour_metrics
              minute_metrics_to_xml(properties.minute_metrics, xml) if properties.minute_metrics
              cors_to_xml(properties.cors, xml) if properties.cors
            }
          end
          builder.to_xml
        end

        def service_properties_from_xml(xml)
          xml = slopify(xml)
          expect_node("StorageServiceProperties", xml)

          StorageServiceProperties.new do |props|
            props.default_service_version = xml.DefaultServiceVersion.text if (xml > "DefaultServiceVersion").any?
            props.logging = logging_from_xml(xml.Logging) if (xml > "Logging").any?
            props.hour_metrics = metrics_from_xml(xml.HourMetrics) if (xml > "HourMetrics").any?
            props.minute_metrics = metrics_from_xml(xml.MinuteMetrics) if (xml > "MinuteMetrics").any?
            props.cors = cors_from_xml(xml.Cors) if (xml > "Cors").any?
          end
        end

        def service_stats_from_xml(xml)
          xml = slopify(xml)
          expect_node("StorageServiceStats", xml)

          StorageServiceStats.new do |stats|
            stats.geo_replication = geo_replication_from_xml(xml.GeoReplication)
          end
        end

        def to_bool(s)
          (s || "").downcase == "true"
        end

        def slopify(xml)
          node = (xml.is_a? String) ? Nokogiri.Slop(xml).root : xml
          node.slop! if node.is_a? Nokogiri::XML::Document unless node.respond_to? :method_missing
          node = node.root if node.is_a? Nokogiri::XML::Document
          node
        end

        def expect_node(node_name, xml)
          raise "Xml is not a #{node_name} node. xml:\n#{xml}" unless xml.name == node_name
        end
      end

      extend ClassMethods

      def self.included(other)
        other.extend(ClassMethods)
      end
    end
  end
end
