require 'log4r'
require 'vagrant/util/retryable'
require 'vagrant-cloudstack/exceptions/exceptions'
require 'vagrant-cloudstack/util/timer'
require 'vagrant-cloudstack/model/cloudstack_resource'
require 'vagrant-cloudstack/service/cloudstack_resource_service'

module VagrantPlugins
  module Cloudstack
    module Action
      # This runs the configured instance.
      class RunInstance
        include Vagrant::Util::Retryable
        include VagrantPlugins::Cloudstack::Model
        include VagrantPlugins::Cloudstack::Service
        include VagrantPlugins::Cloudstack::Exceptions

        def initialize(app, env)
          @app              = app
          @logger           = Log4r::Logger.new('vagrant_cloudstack::action::run_instance')
          @resource_service = CloudstackResourceService.new(env[:cloudstack_compute], env[:ui])
          @security_groups = []
        end

        def call(env)
          # Initialize metrics if they haven't been
          env[:metrics] ||= {}
          @env = env

          sanatize_creation_parameters

          show_creation_summary

          create_vm

          wait_for_instance_ready

          store_volumes

          store_password

          configure_networking

          unless @env[:interrupted]
            wait_for_communicator_ready
            @env[:ui].info(I18n.t('vagrant_cloudstack.ready'))
          end

          # Terminate the instance if we were interrupted
          terminate if @env[:interrupted]

          @app.call(@env)
        end

        def sanatize_creation_parameters
          # Get the domain we're going to booting up in
          @domain = @env[:machine].provider_config.domain_id
          # Get the configs
          @domain_config = @env[:machine].provider_config.get_domain_config(@domain)

          sanitize_domain_config

          initialize_parameters

          if @zone.is_undefined?
            @env[:ui].error("No Zone specified!")
            exit(false)
          end

          resolve_parameters

          cs_zone = @env[:cloudstack_compute].zones.find {|f| f.id == @zone.id}
          resolve_network(cs_zone)

          resolve_security_groups(cs_zone)

          @domain_config.display_name = generate_display_name if @domain_config.display_name.nil?

          if @domain_config.keypair.nil? && @domain_config.ssh_key.nil?
            @env[:ui].warn(I18n.t('vagrant_cloudstack.launch_no_keypair_no_sshkey'))
            generate_ssh_keypair("vagacs_#{@domain_config.display_name}_#{sprintf('%04d', rand(9999))}",
                                 nil, @domain, @domain_config.project_id)
          end
        end

        def sanitize_domain_config
          # Accept a single entry as input, convert it to array
          @domain_config.pf_trusted_networks = [@domain_config.pf_trusted_networks] if @domain_config.pf_trusted_networks

          if @domain_config.network_id.nil?
            # Use names if ids are not present
            @domain_config.network_id = []

            if @domain_config.network_name.nil?
              @domain_config.network_name = []
            else
              @domain_config.network_name = Array(@domain_config.network_name)
            end
          else
            # Use ids if present
            @domain_config.network_id = Array(@domain_config.network_id)
            @domain_config.network_name = []
          end
        end

        def initialize_parameters
          @zone = CloudstackResource.new(@domain_config.zone_id, @domain_config.zone_name, 'zone')
          @networks = CloudstackResource.create_list(@domain_config.network_id, @domain_config.network_name, 'network')
          @service_offering = CloudstackResource.new(@domain_config.service_offering_id, @domain_config.service_offering_name, 'service_offering')
          @disk_offering = CloudstackResource.new(@domain_config.disk_offering_id, @domain_config.disk_offering_name, 'disk_offering')
          @template = CloudstackResource.new(@domain_config.template_id, @domain_config.template_name || @env[:machine].config.vm.box, 'template')
          @pf_ip_address = CloudstackResource.new(@domain_config.pf_ip_address_id, @domain_config.pf_ip_address, 'public_ip_address')
        end

        def resolve_parameters
          begin
            @resource_service.sync_resource(@zone, {available: true, name: @zone.name})
            @resource_service.sync_resource(@service_offering, {listall: true, name: @service_offering.name})
            @resource_service.sync_resource(@disk_offering, {listall: true, name: @disk_offering.name})
            @resource_service.sync_resource(@template, {zoneid: @zone.id, templatefilter: 'executable', listall: true, name: @template.name})
            @resource_service.sync_resource(@pf_ip_address)
          rescue CloudstackResourceNotFound => e
            @env[:ui].error(e.message)
            exit(false)
          end
        end

        def resolve_network(cs_zone)
          if cs_zone.network_type.downcase == 'basic'
            # No network specification in basic zone
            @env[:ui].warn(I18n.t('vagrant_cloudstack.basic_network', :zone_name => @zone.name)) if !@networks.empty? && (@networks[0].id || @networks[0].name)
            @networks = [CloudstackResource.new(nil, nil, 'network')]

            # No portforwarding in basic zone, so none of the below
            @domain_config.pf_ip_address = nil
            @domain_config.pf_ip_address_id = nil
            @domain_config.pf_public_port = nil
            @domain_config.pf_public_rdp_port = nil
            @domain_config.pf_public_port_randomrange = nil
          else
            @networks.each do |network|
              @resource_service.sync_resource(network)
            end
          end
        end

        def resolve_security_groups(cs_zone)
          if cs_zone.security_groups_enabled
            prepare_security_groups
          else
            if !@domain_config.security_group_ids.empty? || !@domain_config.security_group_names.empty? || !@domain_config.security_groups.empty?
              @env[:ui].warn(I18n.t('vagrant_cloudstack.security_groups_disabled', :zone_name => @zone.name))
            end
            @domain_config.security_group_ids = []
            @domain_config.security_group_names = []
            @domain_config.security_groups = []
          end
        end

        def prepare_security_groups
          # Can't use Security Group IDs and Names at the same time
          # Let's use IDs by default...
          if @domain_config.security_group_ids.empty? and !@domain_config.security_group_names.empty?
            @security_groups = @domain_config.security_group_names.map do |name|
              group = CloudstackResource.new(nil, name, 'security_group')
              @resource_service.sync_resource(group)
              group
            end
          elsif !@domain_config.security_group_ids.empty?
            @security_groups = @domain_config.security_group_ids.map do |id|
              group = CloudstackResource.new(id, nil, 'security_group')
              @resource_service.sync_resource(group)
              group
            end
          end

          # Still no security group ids huh?
          # Let's try to create some security groups from specifcation, if provided.
          if !@domain_config.security_groups.empty? and @security_groups.empty?
            @domain_config.security_groups.each do |security_group|
              security_group = create_security_group( security_group)
              @security_groups.push(security_group)
            end
          end
        end

        def create_security_group(security_group)
          begin
            sgid = @env[:cloudstack_compute].create_security_group(:name        => security_group[:name],
                                                                   :description => security_group[:description])['createsecuritygroupresponse']['securitygroup']['id']
            security_group_object = CloudstackResource.new(sgid, security_group[:name], 'security_group')
            @env[:ui].info(" -- Security Group #{security_group[:name]} created with ID: #{sgid}")
          rescue Exception => e
            if e.message =~ /already exis/
              security_group_object = CloudstackResource.new(nil, security_group[:name], 'security_group')
              @resource_service.sync_resource(security_group_object)
              @env[:ui].info(" -- Security Group #{security_group_object.name} found with ID: #{security_group_object.id}")
            end
          end

          # security group is created and we have it's ID
          # so we add the rules... Does it really matter if they already exist ? CLoudstack seems to take care of that!
          security_group[:rules].each do |rule|
            rule_options = {
                :securityGroupId => security_group_object.id,
                :protocol        => rule[:protocol],
                :startport       => rule[:startport],
                :endport         => rule[:endport],
                :cidrlist        => rule[:cidrlist]
            }

            # The rule[:type] is either ingress or egress, but the method call looks the same.
            # We build a dynamic method name and then send it off.
            @env[:cloudstack_compute].send("authorize_security_group_#{rule[:type]}".to_sym, rule_options)
            @env[:ui].info(" --- #{rule[:type].capitalize} Rule added: #{rule[:protocol]} from #{rule[:startport]} to #{rule[:endport]} (#{rule[:cidrlist]})")
          end

          store_security_groups(security_group_object)
        end

        def generate_display_name
          local_user = ENV['USER'] ? ENV['USER'].dup : 'VACS'
          local_user.gsub!(/[^-a-z0-9_]/i, '')
          prefix = @env[:root_path].basename.to_s
          prefix.gsub!(/[^-a-z0-9_]/i, '')

          local_user + '_' + prefix + "_#{Time.now.to_i}"
        end

        def show_creation_summary
          # Launch!
          @env[:ui].info(I18n.t('vagrant_cloudstack.launching_instance'))
          @env[:ui].info(" -- Display Name: #{@domain_config.display_name}")
          @env[:ui].info(" -- Group: #{@domain_config.group}") if @domain_config.group
          @env[:ui].info(" -- Service offering: #{@service_offering.name} (#{@service_offering.id})")
          @env[:ui].info(" -- Disk offering: #{@disk_offering.name} (#{@disk_offering.id})") unless @disk_offering.id.nil?
          @env[:ui].info(" -- Template: #{@template.name} (#{@template.id})")
          @env[:ui].info(" -- Project UUID: #{@domain_config.project_id}") unless @domain_config.project_id.nil?
          @env[:ui].info(" -- Zone: #{@zone.name} (#{@zone.id})")
          @networks.each do |network|
            @env[:ui].info(" -- Network: #{network.name} (#{network.id})")
          end
          @env[:ui].info(" -- Keypair: #{@domain_config.keypair}") if @domain_config.keypair
          @env[:ui].info(' -- User Data: Yes') if @domain_config.user_data
          @security_groups.each do |security_group|
            @env[:ui].info(" -- Security Group: #{security_group.name} (#{security_group.id})")
          end
        end

        def create_vm
          @server = nil
          begin
            options = compose_server_creation_options

            @server = @env[:cloudstack_compute].servers.create(options)
            @server_job_id = @server.job_id
          rescue Fog::Compute::Cloudstack::NotFound => e
            # Invalid subnet doesn't have its own error so we catch and
            # check the error message here.
            # XXX FIXME vpc?
            if e.message =~ /subnet ID/
              raise Errors::FogError,
                    :message => "Subnet ID not found: #{@networks.map(&:id).compact.join(",")}"
            end

            raise
          rescue Fog::Compute::Cloudstack::Error => e
            raise Errors::FogError, :message => e.message
          end

          @env[:machine].id = @server.id
        end

        def compose_server_creation_options
          options = {
              :display_name => @domain_config.display_name,
              :group => @domain_config.group,
              :zone_id => @zone.id,
              :flavor_id => @service_offering.id,
              :image_id => @template.id
          }

          unless @networks.empty?
            nets = @networks.map(&:id).compact.join(",")
            options['network_ids'] = nets unless nets.empty?
          end
          options['security_group_ids'] = @security_groups.map {|security_group| security_group.id}.join(',') unless @security_groups.empty?
          options['project_id'] = @domain_config.project_id unless @domain_config.project_id.nil?
          options['key_name'] = @domain_config.keypair unless @domain_config.keypair.nil?
          options['name'] = @domain_config.name unless @domain_config.name.nil?
          options['ip_address'] = @domain_config.private_ip_address unless @domain_config.private_ip_address.nil?
          options['disk_offering_id'] = @disk_offering.id unless @disk_offering.id.nil?

          if @domain_config.user_data != nil
            options['user_data'] = Base64.urlsafe_encode64(@domain_config.user_data)
            if options['user_data'].length > 2048
              raise Errors::UserdataError,
                    :userdataLength => options['user_data'].length
            end
          end
          options
        end

        def configure_networking
          begin
            enable_static_nat_rules

            configure_port_forwarding

            # First create_port_forwardings,
            # as it may generate 'pf_public_port' or 'pf_public_rdp_port',
            # which after this may need a firewall rule
            configure_firewall

          rescue CloudstackResourceNotFound => e
            @env[:ui].error(e.message)
            terminate
            exit(false)
          end
        end

        def enable_static_nat_rules
          unless @domain_config.static_nat.empty?
            @domain_config.static_nat.each do |rule|
              enable_static_nat(rule)
            end
          end
        end

        def enable_static_nat(rule)
          @env[:ui].info(I18n.t('vagrant_cloudstack.enabling_static_nat'))

          begin
            ip_address = sync_ip_address(rule[:ipaddressid], rule[:ipaddress])
          rescue IpNotFoundException
            return
          end

          @env[:ui].info(" -- IP address : #{ip_address.name} (#{ip_address.id})")

          options = {
              :command          => 'enableStaticNat',
              :ipaddressid      => ip_address_id,
              :virtualmachineid => @env[:machine].id
          }

          begin
            resp = @env[:cloudstack_compute].request(options)
            is_success = resp['enablestaticnatresponse']['success']

            if is_success != 'true'
              @env[:ui].warn(" -- Failed to enable static nat: #{resp['enablestaticnatresponse']['errortext']}")
              return
            end
          rescue Fog::Compute::Cloudstack::Error => e
            raise Errors::FogError, :message => e.message
          end

          # Save ipaddress id to the data dir so it can be disabled when the instance is destroyed
          static_nat_file = @env[:machine].data_dir.join('static_nat')
          static_nat_file.open('a+') do |f|
            f.write("#{ip_address.id}\n")
          end
        end

        def configure_port_forwarding

          unless @pf_ip_address.is_undefined?
            evaluate_pf_private_port
            evaluate_pf_private_rdp_port
            generate_and_apply_port_forwarding_rules_for_communicators
          end

          apply_port_forwarding_rules
        end

        def get_communicator_short_name(communicator)
          communicator_short_names = {
            'VagrantPlugins::CommunicatorSSH::Communicator' => 'ssh',
            'VagrantPlugins::CommunicatorWinRM::Communicator' => 'winrm'
          }
          communicator_short_names[communicator.class.name]
        end

        def evaluate_pf_private_port
          return unless @domain_config.pf_private_port.nil?

          communicator_short_name = get_communicator_short_name(@env[:machine].communicate)
          communicator_config = @env[:machine].config.send(communicator_short_name)

          @domain_config.pf_private_port = communicator_config.port if communicator_config.respond_to?('port')
          @domain_config.pf_private_port = communicator_config.guest_port if communicator_config.respond_to?('guest_port')
          @domain_config.pf_private_port = communicator_config.default.port if (communicator_config.respond_to?('default') && communicator_config.default.respond_to?('port'))
        end

        def evaluate_pf_private_rdp_port
          @domain_config.pf_private_rdp_port = @env[:machine].config.vm.rdp.port if
                                                  @env[:machine].config.vm.respond_to?(:rdp) &&
                                                  @env[:machine].config.vm.rdp.respond_to?(:port)
        end

        def generate_and_apply_port_forwarding_rules_for_communicators
          communicators_port_names = [Hash[:public_port => 'pf_public_port', :private_port => 'pf_private_port']]
          communicators_port_names << Hash[:public_port => 'pf_public_rdp_port', :private_port => 'pf_private_rdp_port'] if is_windows_guest

          communicators_port_names.each do |communicator_port_names|
            public_port_name = communicator_port_names[:public_port]
            private_port_name = communicator_port_names[:private_port]

            next unless @domain_config.send(public_port_name) || @domain_config.pf_public_port_randomrange

            port_forwarding_rule = {
                :ipaddressid => @domain_config.pf_ip_address_id,
                :ipaddress => @domain_config.pf_ip_address,
                :protocol => 'tcp',
                :publicport => @domain_config.send(public_port_name),
                :privateport => @domain_config.send(private_port_name),
                :openfirewall => @domain_config.pf_open_firewall
            }

            public_port = create_randomport_forwarding_rule(
                port_forwarding_rule,
                @domain_config.pf_public_port_randomrange[:start]...@domain_config.pf_public_port_randomrange[:end],
                public_port_name
            )
            @domain_config.send("#{public_port_name}=", public_port)
          end
        end

        def create_randomport_forwarding_rule(rule, randomrange, filename)
          pf_public_port = rule[:publicport]
          retryable(:on => DuplicatePFRule, :tries => 10) do
            begin
              # Only if public port is missing, will generate a random one
              rule[:publicport] = Kernel.rand(randomrange) if pf_public_port.nil?

              apply_port_forwarding_rule(rule)

              if pf_public_port.nil?
                pf_port_file = @env[:machine].data_dir.join(filename)
                pf_port_file.open('a+') do |f|
                  f.write("#{rule[:publicport]}")
                end
              end
            rescue Errors::FogError => e
              if pf_public_port.nil? && !(e.message =~ /The range specified,.*conflicts with rule.*which has/).nil?
                raise DuplicatePFRule, :message => e.message
              else
                raise Errors::FogError, :message => e.message
              end
            end
          end
          pf_public_port.nil? ? (rule[:publicport]) : (pf_public_port)
        end

        def apply_port_forwarding_rules
          return if @domain_config.port_forwarding_rules.empty?

          @domain_config.port_forwarding_rules.each do |port_forwarding_rule|
            # Sanatize/defaults pf rule before applying
            port_forwarding_rule[:ipaddressid] = @domain_config.pf_ip_address_id if port_forwarding_rule[:ipaddressid].nil?
            port_forwarding_rule[:ipaddress] = @domain_config.pf_ip_address if port_forwarding_rule[:ipaddress].nil?
            port_forwarding_rule[:protocol] = 'tcp' if port_forwarding_rule[:protocol].nil?
            port_forwarding_rule[:openfirewall] = @domain_config.pf_open_firewall if port_forwarding_rule[:openfirewall].nil?
            port_forwarding_rule[:publicport] = port_forwarding_rule[:privateport] if port_forwarding_rule[:publicport].nil?
            port_forwarding_rule[:privateport] = port_forwarding_rule[:publicport] if port_forwarding_rule[:privateport].nil?

            apply_port_forwarding_rule(port_forwarding_rule)
          end
        end

        def apply_port_forwarding_rule(rule)
          port_forwarding_rule = nil
          @env[:ui].info(I18n.t('vagrant_cloudstack.creating_port_forwarding_rule'))

          return unless (options = compose_port_forwarding_rule_creation_options(rule))

          begin
            resp = @env[:cloudstack_compute].create_port_forwarding_rule(options)
            job_id = resp['createportforwardingruleresponse']['jobid']

            if job_id.nil?
              @env[:ui].warn(" -- Failed to create port forwarding rule: #{resp['createportforwardingruleresponse']['errortext']}")
              return
            end

            while true
              response = @env[:cloudstack_compute].query_async_job_result({:jobid => job_id})
              if response['queryasyncjobresultresponse']['jobstatus'] != 0
                port_forwarding_rule = response['queryasyncjobresultresponse']['jobresult']['portforwardingrule']
                break
              else
                sleep 2
              end
            end
          rescue Fog::Compute::Cloudstack::Error => e
            raise Errors::FogError, :message => e.message
          end

          store_port_forwarding_rules(port_forwarding_rule)
        end

        def compose_port_forwarding_rule_creation_options(rule)
          begin
            ip_address = sync_ip_address(rule[:ipaddressid], rule[:ipaddress])
          rescue IpNotFoundException
            return
          end

          @env[:ui].info(" -- IP address    : #{ip_address.name} (#{ip_address.id})")
          @env[:ui].info(" -- Protocol      : #{rule[:protocol]}")
          @env[:ui].info(" -- Public port   : #{rule[:publicport]}")
          @env[:ui].info(" -- Private port  : #{rule[:privateport]}")
          @env[:ui].info(" -- Open Firewall : #{rule[:openfirewall]}")

          network = get_network_from_public_ip(ip_address)

          options = {
              :networkid => network.id,
              :ipaddressid => ip_address.id,
              :publicport => rule[:publicport],
              :privateport => rule[:privateport],
              :protocol => rule[:protocol],
              :openfirewall => rule[:openfirewall],
              :virtualmachineid => @env[:machine].id
          }

          options.delete(:openfirewall) if network.details.has_key?('vpcid')
          options
        end

        def get_network_from_public_ip(ip_address)
          if ip_address.details.has_key?('associatednetworkid')
            network = @networks.find {|f| f.id == ip_address.details['associatednetworkid']}
          elsif ip_address.details.has_key?('vpcid')
            # In case of VPC and ip has not yet been used, a network MUST be specified
            network = @networks.find {|f| f.details['vpcid'] == ip_address.details['vpcid']}
          end
          network
        end

        def configure_firewall
          if @domain_config.pf_trusted_networks
            generate_firewall_rules_for_communicators unless @pf_ip_address.is_undefined?
            generate_firewall_rules_for_portforwarding_rules
          end

          return if @domain_config.firewall_rules.empty?
          auto_complete_firewall_rules
          apply_firewall_rules
        end

        def generate_firewall_rules_for_communicators

          return if @pf_ip_address.is_undefined? ||
              !@domain_config.pf_trusted_networks ||
              @domain_config.pf_open_firewall

          ports = [Hash[publicport: 'pf_public_port', privateport: 'pf_private_port']]
          ports << Hash[publicport: 'pf_public_rdp_port', privateport: 'pf_private_rdp_port'] if is_windows_guest

          ports.each do |port_set|
            forward_portname = @pf_ip_address.details.key?('vpcid') ? port_set[:privateport] : port_set[:publicport]
            check_portname = port_set[:publicport]

            next unless @domain_config.send(check_portname)

            # Allow access to public port from trusted networks only
            fw_rule_trusted_networks = {
                ipaddressid: @pf_ip_address.id,
                ipaddress: @pf_ip_address.name,
                protocol: 'tcp',
                startport: @domain_config.send(forward_portname),
                endport: @domain_config.send(forward_portname),
                cidrlist: @domain_config.pf_trusted_networks.join(',')
            }
            @domain_config.firewall_rules = [] unless @domain_config.firewall_rules
            @domain_config.firewall_rules << fw_rule_trusted_networks

          end
        end

        def generate_firewall_rules_for_portforwarding_rules
          @pf_ip_address.details.has_key?('vpcid') ? port_name = :privateport : port_name = :publicport

          unless @domain_config.port_forwarding_rules.empty?
            @domain_config.port_forwarding_rules.each do |port_forwarding_rule|
              if port_forwarding_rule[:generate_firewall] && @domain_config.pf_trusted_networks && !port_forwarding_rule[:openfirewall]
                # Allow access to public port from trusted networks only
                fw_rule_trusted_networks = {
                    :ipaddressid => port_forwarding_rule[:ipaddressid],
                    :ipaddress => port_forwarding_rule[:ipaddress],
                    :protocol => port_forwarding_rule[:protocol],
                    :startport => port_forwarding_rule[port_name],
                    :endport => port_forwarding_rule[port_name],
                    :cidrlist => @domain_config.pf_trusted_networks.join(',')
                }
                @domain_config.firewall_rules = [] unless @domain_config.firewall_rules
                @domain_config.firewall_rules << fw_rule_trusted_networks
              end
            end
          end
        end

        def auto_complete_firewall_rules
          @domain_config.firewall_rules.each do |firewall_rule|
            firewall_rule[:ipaddressid] = @domain_config.pf_ip_address_id if firewall_rule[:ipaddressid].nil?
            firewall_rule[:ipaddress] = @domain_config.pf_ip_address if firewall_rule[:ipaddress].nil?
            firewall_rule[:cidrlist] = @domain_config.pf_trusted_networks.join(',') if firewall_rule[:cidrlist].nil?
            firewall_rule[:protocol] = 'tcp' if firewall_rule[:protocol].nil?
            firewall_rule[:startport] = firewall_rule[:endport] if firewall_rule[:startport].nil?
          end
        end

        def apply_firewall_rules
          @domain_config.firewall_rules.each do |firewall_rule|
            create_firewall_rule(firewall_rule)
          end
        end

        def create_firewall_rule(rule)
          acl_name = ''
          firewall_rule = nil
          @env[:ui].info(I18n.t('vagrant_cloudstack.creating_firewall_rule'))

          ip_address = CloudstackResource.new(rule[:ipaddressid], rule[:ipaddress], 'public_ip_address')
          @resource_service.sync_resource(ip_address)

          @env[:ui].info(" -- IP address : #{ip_address.name} (#{ip_address.id})")
          @env[:ui].info(" -- Protocol   : #{rule[:protocol]}")
          @env[:ui].info(" -- CIDR list  : #{rule[:cidrlist]}")
          @env[:ui].info(" -- Start port : #{rule[:startport]}")
          @env[:ui].info(" -- End port   : #{rule[:endport]}")
          @env[:ui].info(" -- ICMP code  : #{rule[:icmpcode]}")
          @env[:ui].info(" -- ICMP type  : #{rule[:icmptype]}")

          if ip_address.details.has_key?('vpcid')
            network = @networks.find{ |f| f.id == ip_address.details['associatednetworkid']}
            acl_id = network.details['aclid']

            raise CloudstackResourceNotFound.new("No ACL found associated with VPC tier #{network.details['name']} (id: #{network.details['id']})") unless acl_id

            resp = @env[:cloudstack_compute].list_network_acl_lists(
                id:  network.details[acl_id]
            )
            acl_name = resp['listnetworkacllistsresponse']['networkacllist'][0]['name']

            options, response_string, type_string = compose_firewall_rule_creation_options_vpc(network, rule)
          else
            options, response_string, type_string = compose_firewall_rule_creation_options_non_vpc(ip_address, rule)
          end

          firewall_rule = apply_firewall_rule(acl_name, options, response_string, type_string)

          store_firewall_rule(firewall_rule, type_string) if firewall_rule
        end

        def compose_firewall_rule_creation_options_vpc(network, rule)
          command_string = 'createNetworkACL'
          response_string = 'createnetworkaclresponse'
          type_string = 'networkacl'
          options = {
              :command => command_string,
              :aclid => network.details['aclid'],
              :action => 'Allow',
              :protocol => rule[:protocol],
              :cidrlist => rule[:cidrlist],
              :startport => rule[:startport],
              :endport => rule[:endport],
              :icmpcode => rule[:icmpcode],
              :icmptype => rule[:icmptype],
              :traffictype => 'Ingress'
          }
          return options, response_string, type_string
        end

        def compose_firewall_rule_creation_options_non_vpc(ip_address, rule)
          command_string = 'createFirewallRule'
          response_string = 'createfirewallruleresponse'
          type_string = 'firewallrule'
          options = {
              :command => command_string,
              :ipaddressid => ip_address.id,
              :protocol => rule[:protocol],
              :cidrlist => rule[:cidrlist],
              :startport => rule[:startport],
              :endeport => rule[:endport],
              :icmpcode => rule[:icmpcode],
              :icmptype => rule[:icmptype]
          }
          return options, response_string, type_string
        end

        def get_next_free_acl_entry(network)
          resp = @env[:cloudstack_compute].list_network_acls(
              aclid: network.details['aclid']
          )
          number = 0
          if resp["listnetworkaclsresponse"].key?("networkacl")
            resp["listnetworkaclsresponse"]["networkacl"].each {|ace| number = [number, ace["number"]].max}
          end
          number = number+1
        end

        def apply_firewall_rule(acl_name, options, response_string, type_string)
          firewall_rule = nil
          begin
            resp = @env[:cloudstack_compute].request(options)
            job_id = resp[response_string]['jobid']

            if job_id.nil?
              @env[:ui].warn(" -- Failed to create firewall rule: #{resp[response_string]['errortext']}")
              return
            end

            while true
              response = @env[:cloudstack_compute].query_async_job_result({:jobid => job_id})
              if response['queryasyncjobresultresponse']['jobstatus'] != 0
                firewall_rule = response['queryasyncjobresultresponse']['jobresult'][type_string]
                break
              else
                sleep 2
              end
            end
          rescue Fog::Compute::Cloudstack::Error => e
            if e.message =~ /The range specified,.*conflicts with rule/
              @env[:ui].warn(" -- Failed to create firewall rule: #{e.message}")
            elsif e.message =~ /Default ACL cannot be modified/
              @env[:ui].warn(" -- Failed to create network acl: #{e.message}: #{acl_name}")
            else
              raise Errors::FogError, :message => e.message
            end
          end
          firewall_rule
        end

        def is_windows_guest
          false || @env[:machine].config.vm.guest == :windows || get_communicator_short_name(@env[:machine].communicate) == 'winrm'
        end

        def generate_ssh_keypair(keyname, account = nil, domainid = nil, projectid = nil)
          response = @env[:cloudstack_compute].create_ssh_key_pair(keyname, account, domainid, projectid)
          sshkeypair = response['createsshkeypairresponse']['keypair']

          # Save private key to file
          sshkeyfile_file = @env[:machine].data_dir.join('sshkeyfile')
          sshkeyfile_file.open('w') do |f|
            f.write("#{sshkeypair['privatekey']}")
          end
          @domain_config.ssh_key = sshkeyfile_file.to_s

          sshkeyname_file = @env[:machine].data_dir.join('sshkeyname')
          sshkeyname_file.open('w') do |f|
            f.write("#{sshkeypair['name']}")
          end

          @domain_config.keypair =  sshkeypair['name']
        end

        def store_password
          password = nil
          if @server.password_enabled and @server.respond_to?('job_id')
            server_job_result = @env[:cloudstack_compute].query_async_job_result({:jobid => @server_job_id})
            if server_job_result.nil?
              @env[:ui].warn(' -- Failed to retrieve job_result for retrieving the password')
              return
            end

            while true
              server_job_result = @env[:cloudstack_compute].query_async_job_result({:jobid => @server_job_id})
              if server_job_result['queryasyncjobresultresponse']['jobstatus'] != 0
                password = server_job_result['queryasyncjobresultresponse']['jobresult']['virtualmachine']['password']
                break
              else
                sleep 2
              end
            end

            @env[:ui].info("Password of virtualmachine: #{password}")
            # Set the password on the current communicator
            @domain_config.vm_password = password

            # Save password to file
            vmcredentials_file = @env[:machine].data_dir.join('vmcredentials')
            vmcredentials_file.open('w') do |f|
              f.write("#{password}")
            end
          end
        end

        def store_volumes
          volumes = @env[:cloudstack_compute].volumes.find_all { |f| f.server_id == @server.id }
          # volumes refuses to be iterated directly, do it by index
          (0...volumes.length).each do |idx|
            unless volumes[idx].type == 'ROOT'
              volumes_file = @env[:machine].data_dir.join('volumes')
              volumes_file.open('a+') do |f|
                f.write("#{volumes[idx].id}\n")
              end
            end
          end
        end

        def store_firewall_rule(firewall_rule, type_string)
          unless firewall_rule.nil?
            # Save firewall rule id to the data dir so it can be released when the instance is destroyed
            firewall_file = @env[:machine].data_dir.join('firewall')
            firewall_file.open('a+') do |f|
              f.write("#{firewall_rule['id']},#{type_string}\n")
            end
          end
        end

        def store_port_forwarding_rules(port_forwarding_rule)
          port_forwarding_file = @env[:machine].data_dir.join('port_forwarding')
          port_forwarding_file.open('a+') do |f|
            f.write("#{port_forwarding_rule['id']}\n")
          end
        end

        def store_security_groups(security_group_object)
          security_groups_file = @env[:machine].data_dir.join('security_groups')
          security_groups_file.open('a+') do |f|
            f.write("#{security_group_object.id}\n")
          end
          security_group_object
        end

        def wait_for_communicator_ready
          @env[:metrics]['instance_ssh_time'] = Util::Timer.time do
            # Wait for communicator to be ready.
            communicator_short_name = get_communicator_short_name(@env[:machine].communicate)
            @env[:ui].info(
              I18n.t('vagrant_cloudstack.waiting_for_communicator',
                     communicator: communicator_short_name.to_s.upcase)
            )
            while true
              # If we're interrupted then just back out
              break if @env[:interrupted]
              break if @env[:machine].communicate.ready?
              sleep 2
            end
          end
          @logger.info("Time for SSH ready: #{@env[:metrics]['instance_ssh_time']}")
        end

        def wait_for_instance_ready
          @env[:metrics]['instance_ready_time'] = Util::Timer.time do
            tries = @domain_config.instance_ready_timeout / 2

            @env[:ui].info(I18n.t('vagrant_cloudstack.waiting_for_ready'))
            begin
              retryable(:on => Fog::Errors::TimeoutError, :tries => tries) do
                # If we're interrupted don't worry about waiting
                next if @env[:interrupted]

                # Wait for the server to be ready
                @server.wait_for(2) { ready? }
              end
            rescue Fog::Errors::TimeoutError
              # Delete the instance
              terminate

              # Notify the user
              raise Errors::InstanceReadyTimeout,
                    :timeout => @domain_config.instance_ready_timeout
            end
          end
          @logger.info("Time to instance ready: #{@env[:metrics]['instance_ready_time']}")
        end

        def recover(env)
          return if env['vagrant.error'].is_a?(Vagrant::Errors::VagrantError)

          if env[:machine].provider.state.id != :not_created
            # Undo the import
            terminate
          end
        end

        def terminate
          destroy_env = @env.dup
          destroy_env.delete(:interrupted)
          destroy_env[:config_validate]       = false
          destroy_env[:force_confirm_destroy] = true
          @env[:action_runner].run(Action.action_destroy, destroy_env)
        end

        private

        def sync_ip_address(ip_address_id, ip_address_value)
          ip_address = CloudstackResource.new(ip_address_id, ip_address_value, 'public_ip_address')

          if ip_address.is_undefined?
            message = 'IP address is not specified. Skip creating port forwarding rule.'
            @logger.info(message)
            @env[:ui].info(I18n.t(message))
            raise IpNotFoundException
          end

          @resource_service.sync_resource(ip_address)

          ip_address
        end
      end
    end
  end
end
