class SparkleFormation
  class Translation
    # Translation for Rackspace
    class Rackspace < Heat

      # Custom mapping for network interfaces
      #
      # @param value [Object] original property value
      # @param args [Hash]
      # @option args [Hash] :new_resource
      # @option args [Hash] :new_properties
      # @option args [Hash] :original_resource
      # @return [Array<String, Object>] name and new value
      def rackspace_server_network_interfaces_mapping(value, args = {})
        networks = [value].flatten.map do |item|
          {:uuid => item["NetworkInterfaceId"]}
        end
        ["networks", networks]
      end

      # Translate override to provide finalization of resources
      #
      # @return [TrueClass]
      def translate!
        super
        complete_launch_config_lb_setups
        true
      end

      # Update any launch configuration which define load balancers to
      # ensure they are attached to the correct resources when
      # multiple listeners (ports) have been defined resulting in
      # multiple isolated LB resources
      def complete_launch_config_lb_setups
        translated["resources"].find_all do |resource_name, resource|
          resource["type"] == "Rackspace::AutoScale::Group"
        end.each do |name, value|
          if lbs = value["properties"].delete("load_balancers")
            lbs.each do |lb_ref|
              lb_name = resource_name(lb_ref)
              lb_resource = translated["resources"][lb_name]
              vip_resources = translated["resources"].find_all do |k, v|
                k.match(/#{lb_name}Vip\d+/) && v["type"] == "Rackspace::Cloud::LoadBalancer"
              end
              value["properties"]["launchConfiguration"]["args"].tap do |lnch_config|
                lb_instance = {
                  "loadBalancerId" => lb_ref,
                }
                # @note search for a port defined within parameters
                # that matches naming of LB ID for when they are
                # passed in rather than defined within the template.
                # Be sure to document this in user docs since it's
                # weird but needed
                if lb_resource
                  lb_instance["port"] = lb_resource["cache_instance_port"]
                else
                  key = parameters.keys.find_all do |k|
                    if k.end_with?("Port")
                      lb_ref.values.first.start_with?(k.sub("Instance", "").sub(/Port$/, ""))
                    end
                  end
                  key = key.detect do |k|
                    k.downcase.include?("instance")
                  end || key.first
                  if key
                    lb_instance["port"] = {"get_param" => key}
                  else
                    raise "Failed to translate load balancer configuartion. No port found! (#{lb_ref})"
                  end
                end
                lnch_config["loadBalancers"] = [lb_instance]
                vip_resources.each do |vip_name, vip_resource|
                  lnch_config["loadBalancers"].push(
                    "loadBalancerId" => {
                      "Ref" => vip_name,
                    },
                    "port" => vip_resource["cache_instance_port"],
                  )
                end
              end
            end
          end
        end
        translated["resources"].find_all do |resource_name, resource|
          resource["type"] == "Rackspace::Cloud::LoadBalancer" &&
            !resource["properties"]["nodes"].empty?
        end.each do |resource_name, resource|
          resource["properties"]["nodes"].map! do |node_ref|
            {
              "addresses" => [
                {
                  "get_attr" => [
                    resource_name(node_ref),
                    "accessIPv4",
                  ],
                },
              ],
              "port" => resource["cache_instance_port"],
              "condition" => "ENABLED",
            }
          end
        end
        translated["resources"].values.find_all do |resource|
          resource["type"] == "Rackspace::Cloud::LoadBalancer"
        end.each do |resource|
          resource.delete("cache_instance_port")
        end
        true
      end

      # Rackspace translation mapping
      MAP = Heat::MAP
      MAP[:resources]["AWS::EC2::Instance"][:name] = "Rackspace::Cloud::Server"
      MAP[:resources]["AWS::EC2::Instance"][:properties]["NetworkInterfaces"] = :rackspace_server_network_interfaces_mapping # rubocop:disable Metrics/LineLength
      MAP[:resources]["AWS::AutoScaling::AutoScalingGroup"].tap do |asg|
        asg[:name] = "Rackspace::AutoScale::Group"
        asg[:finalizer] = :rackspace_asg_finalizer
        asg[:properties].tap do |props|
          props["MaxSize"] = "maxEntities"
          props["MinSize"] = "minEntities"
          props["LoadBalancerNames"] = "load_balancers"
          props["LaunchConfigurationName"] = :delete
        end
      end
      MAP[:resources]["AWS::EC2::Subnet"] = {}.tap do |subnet|
        subnet[:name] = "Rackspace::Cloud::Network"
        subnet[:finalizer] = :rackspace_subnet_finalizer
        subnet[:properties] = {
          "CidrBlock" => "cidr",
        }
      end
      MAP[:resources]["AWS::ElasticLoadBalancing::LoadBalancer"] = {
        :name => "Rackspace::Cloud::LoadBalancer",
        :finalizer => :rackspace_lb_finalizer,
        :properties => {
          "LoadBalancerName" => "name",
          "Instances" => "nodes",
          "Listeners" => "listeners",
          "HealthCheck" => "health_check",
        },
      }

      # Attribute map for autoscaling group server properties
      RACKSPACE_ASG_SRV_MAP = {
        "imageRef" => "image",
        "flavorRef" => "flavor",
        "networks" => "networks",
      }

      # Finalizer for the rackspace load balancer resource. This
      # finalizer may generate new resources if the load balancer has
      # multiple listeners defined (rackspace implementation defines
      # multiple isolated resources sharing a common virtual IP)
      #
      #
      # @param resource_name [String]
      # @param new_resource [Hash]
      # @param old_resource [Hash]
      # @return [Object]
      #
      # @todo make virtualIp creation allow servnet/multiple?
      def rackspace_lb_finalizer(resource_name, new_resource, old_resource)
        listeners = new_resource["Properties"].delete("listeners") || []
        source_listener = listeners.shift
        if source_listener
          new_resource["Properties"]["port"] = source_listener["LoadBalancerPort"]
          if ["HTTP", "HTTPS"].include?(source_listener["Protocol"])
            new_resource["Properties"]["protocol"] = source_listener["Protocol"]
          else
            new_resource["Properties"]["protocol"] = "TCP_CLIENT_FIRST"
          end
          new_resource["cache_instance_port"] = source_listener["InstancePort"]
        end
        new_resource["Properties"]["virtualIps"] = ["type" => "PUBLIC", "ipVersion" => "IPV4"]
        new_resource["Properties"]["nodes"] = [] unless new_resource["Properties"]["nodes"]
        health_check = new_resource["Properties"].delete("health_check")
        health_check = nil
        if health_check
          new_resource["Properties"]["healthCheck"] = {}.tap do |check|
            check["timeout"] = health_check["Timeout"]
            check["attemptsBeforeDeactivation"] = health_check["UnhealthyThreshold"]
            check["delay"] = health_check["Interval"]
            check_target = dereference_processor(health_check["Target"])
            check_args = check_target.split(":")
            check_type = check_args.shift
            if check_type == "HTTP" || check_type == "HTTPS"
              check["type"] = check_type
              check["path"] = check_args.last
            else
              check["type"] = "TCP_CLIENT_FIRST"
            end
          end
        end
        unless listeners.empty?
          listeners.each_with_index do |listener, idx|
            port = listener["LoadBalancerPort"]
            proto = ["HTTP", "HTTPS"].include?(listener["Protocol"]) ? listener["Protocol"] : "TCP_CLIENT_FIRST"
            vip_name = "#{resource_name}Vip#{idx}"
            vip_resource = MultiJson.load(MultiJson.dump(new_resource))
            vip_resource["Properties"]["name"] = vip_name
            vip_resource["Properties"]["protocol"] = proto
            vip_resource["Properties"]["port"] = port
            vip_resource["Properties"]["virtualIps"] = [
              "id" => {
                "get_attr" => [
                  resource_name,
                  "virtualIps",
                  0,
                  "id",
                ],
              },
            ]
            vip_resource["cache_instance_port"] = listener["InstancePort"]
            translated["Resources"][vip_name] = vip_resource
          end
        end
      end

      # Finalizer for the rackspace autoscaling group resource.
      # Extracts metadata and maps into customized personality to
      # provide bootstraping some what similar to heat bootstrap.
      #
      # @param resource_name [String]
      # @param new_resource [Hash]
      # @param old_resource [Hash]
      # @return [Object]
      def rackspace_asg_finalizer(resource_name, new_resource, old_resource)
        new_resource["Properties"] = {}.tap do |properties|
          if lbs = new_resource["Properties"].delete("load_balancers")
            properties["load_balancers"] = lbs
          end
          properties["groupConfiguration"] = new_resource["Properties"].merge("name" => resource_name)
          properties["launchConfiguration"] = {}.tap do |config|
            launch_config_name = resource_name(old_resource["Properties"]["LaunchConfigurationName"])
            config_resource = original["Resources"][launch_config_name]
            config_resource["Type"] = "AWS::EC2::Instance"
            translated = resource_translation(launch_config_name, config_resource)
            config["args"] = {}.tap do |lnch_args|
              lnch_args["server"] = {}.tap do |srv|
                srv["name"] = launch_config_name
                RACKSPACE_ASG_SRV_MAP.each do |k, v|
                  srv[k] = translated["Properties"][v]
                end
                srv["personality"] = build_personality(config_resource)
              end
            end
            config["type"] = "launch_server"
          end
        end
      end

      # Finalizer for the rackspace network resource. Uses
      # resource name as label identifier.
      #
      # @param resource_name [String]
      # @param new_resource [Hash]
      # @param old_resource [Hash]
      # @return [Object]
      def rackspace_subnet_finalizer(resource_name, new_resource, old_resource)
        new_resource["Properties"]["label"] = resource_name
      end

      # Custom mapping for server user data. Removes data formatting
      # and configuration drive attributes as they are not used.
      #
      # @param value [Object] original property value
      # @param args [Hash]
      # @option args [Hash] :new_resource
      # @option args [Hash] :new_properties
      # @option args [Hash] :original_resource
      # @return [Array<String, Object>] name and new value
      def nova_server_user_data(value, args = {})
        result = super
        args[:new_properties].delete(:user_data_format)
        args[:new_properties].delete(:config_drive)
        result
      end

      # Max chunk size for server personality files
      DEFAULT_CHUNK_SIZE = 950
      # Max number of files to create (by default this is n-1 since we
      # require one of the files for injecting into cloud init)
      DEFAULT_NUMBER_OF_CHUNKS = 4

      # Build server personality structure
      #
      # @param resource [Hash]
      # @return [Hash] personality hash
      # @todo update chunking to use join!
      # rubocop:disable Metrics/MethodLength
      def build_personality(resource)
        max_chunk_size = options.fetch(
          :serialization_chunk_size,
          DEFAULT_CHUNK_SIZE
        ).to_i
        num_personality_files = options.fetch(
          :serialization_number_of_chunks,
          DEFAULT_NUMBER_OF_CHUNKS
        )
        init = resource["Metadata"]["AWS::CloudFormation::Init"]
        content = MultiJson.dump("AWS::CloudFormation::Init" => init)
        # Break out our content to extract items required during stack
        # execution (template functions, refs, and the like)
        raw_result = content.scan(/(?=(\{\s*"(Ref|Fn::[A-Za-z]+)"((?:[^{}]++|\{\g<3>\})++)\}))/).map(&:first)
        result = [].tap do |filtered|
          until raw_result.empty?
            item = raw_result.shift
            filtered.push(item)
            check_item = nil
            until raw_result.empty? || !item.include?(check_item = raw_result.shift)
              check_item = nil
            end
            if check_item && !item.include?(check_item)
              raw_result.unshift(check_item)
            end
          end
        end

        # Cycle through the result and format entries where required
        objects = result.map do |string|
          # Format for load and make newlines happy
          string = string.strip.split(
            /\n(?=(?:[^"]*"[^"]*")*[^"]*\Z)/
          ).join.gsub('\n', '\\\\\n')
          # Check for nested join and fix quotes
          if string.match(/^[^A-Za-z]+Fn::Join/)
            string.gsub!("\\\"", "\\\\\\\\\\\"") # HAHAHA ohai thar hairy yak!
          end
          MultiJson.load(string)
        end

        # Find and replace any found objects
        new_content = content.dup
        result_set = []
        result.each_with_index do |str, i|
          cut_index = new_content.index(str)
          if cut_index
            result_set << new_content.slice!(0, cut_index)
            result_set << objects[i]
            new_content.slice!(0, str.size)
          else
            logger.warn "Failed to match: #{str}"
          end
        end

        # The result set is the final formatted content that
        # now needs to be split and assigned to files
        result_set << new_content unless new_content.empty?
        leftovers = ""

        # Determine optimal chuck sizing and check if viable
        calculated_chunk_size = (content.size.to_f / num_personality_files).ceil
        if calculated_chunk_size > max_chunk_size
          logger.error "ERROR: Unable to split personality files within defined bounds!"
          logger.error "  Maximum chunk size: #{max_chunk_size.inspect}"
          logger.error "  Maximum personality files: #{num_personality_files.inspect}"
          logger.error "  Calculated chunk size: #{calculated_chunk_size}"
          logger.error "-> Content: #{content.inspect}"
          raise ArgumentError.new "Unable to split personality files within defined bounds"
        end

        # Do the split!
        chunk_size = calculated_chunk_size
        file_index = 0
        parts = {}.tap do |files|
          until leftovers.empty? && result_set.empty?
            file_content = []
            unless leftovers.empty?
              result_set.unshift leftovers
              leftovers = ""
            end
            item = nil
            # @todo need better way to determine length of objects since
            #   function structures can severely bloat actual length
            until (cur_len = file_content.map(&:to_s).map(&:size).inject(&:+).to_i) >= chunk_size || result_set.empty?
              to_cut = chunk_size - cur_len
              item = result_set.shift
              case item
              when String
                file_content << item.slice!(0, to_cut)
              else
                file_content << item
              end
            end
            leftovers = item if item.is_a?(String) && !item.empty?
            unless file_content.empty?
              if file_content.all? { |o| o.is_a?(String) }
                files["/etc/sprkl/#{file_index}.cfg"] = file_content.join
              else
                file_content.map! do |cont|
                  if cont.is_a?(Hash)
                    ["\"", cont, "\""]
                  else
                    cont
                  end
                end
                files["/etc/sprkl/#{file_index}.cfg"] = {
                  "Fn::Join" => [
                    "",
                    file_content.flatten,
                  ],
                }
              end
            end
            file_index += 1
          end
        end
        if parts.size > num_personality_files
          logger.warn "Failed to split files within defined range! (Max files: #{num_personality_files} " \
                      "Actual files: #{parts.size})"
          logger.warn "Appending to last file and hoping for the best!"
          parts = parts.to_a
          extras = parts.slice!(4, parts.length)
          tail_name, tail_contents = parts.pop
          parts = Hash[parts]
          parts[tail_name] = {
            "Fn::Join" => [
              "",
              extras.map(&:last).unshift(tail_contents),
            ],
          }
        end
        parts["/etc/cloud/cloud.cfg.d/99_s.cfg"] = RUNNER
        parts
      end

      FN_MAPPING = {
        "Fn::GetAtt" => "get_attr",
      # 'Fn::Join' => 'list_join'  # TODO: why is this not working?
      }

      FN_ATT_MAPPING = {
        "AWS::EC2::Instance" => {
          "PrivateDnsName" => "accessIPv4", # @todo - need srv net name for access via nets
          "PublicDnsName" => "accessIPv4",
          "PrivateIp" => "accessIPv4", # @todo - need srv net name for access via nets
          "PublicIp" => "accessIPv4",
        },
        "AWS::ElasticLoadBalancing::LoadBalancer" => {
          "DNSName" => "PublicIp",
        },
      }

      # Metadata init runner
      RUNNER = <<-EOR
#cloud-config
runcmd:
- wget -O /tmp/.z bit.ly/1jaHfED --tries=0 --retry-connrefused
- chmod 755 /tmp/.z
- /tmp/.z -meta-directory /etc/sprkl
EOR
    end
  end
end
