# frozen_string_literal: true

require 'log4r'
require 'vagrant/util/network_ip'
require 'vagrant/util/scoped_hash_override'
require 'ipaddr'
require 'thread'

module VagrantPlugins
  module ProviderLibvirt
    module Action
      # Prepare all networks needed for domain connections.
      class CreateNetworks
        include Vagrant::Util::NetworkIP
        include Vagrant::Util::ScopedHashOverride
        include VagrantPlugins::ProviderLibvirt::Util::ErbTemplate
        include VagrantPlugins::ProviderLibvirt::Util::NetworkUtil

        @@lock = Mutex.new

        def initialize(app, env)
          mess = 'vagrant_libvirt::action::create_networks'
          @logger = Log4r::Logger.new(mess)
          @app = app

          @available_networks = []
          @options = {}
          @libvirt_client = env[:machine].provider.driver.connection.client
        end

        def call(env)
          if env[:machine].provider_config.qemu_use_session
            # Get a list of all (active and inactive) Libvirt networks. This
            # triggers a side effect to ensure networking is fully available
            # for VMs using sessions. It is likely that this should be done
            # to determine the correct virtual device for the management
            # network for sessions instead of assuming the default of virbr0.
            @available_networks = libvirt_networks(env[:machine].provider.driver)

            @app.call(env)
            return
          end

          # only one vm at a time should try to set up networks
          # otherwise they'll have inconsistent views of current state
          # and conduct redundant operations that cause errors
          @@lock.synchronize do
            # Iterate over networks If some network is not
            # available, create it if possible. Otherwise raise an error.
            configured_networks(env, @logger).each do |options|
              # Only need to create private networks
              next if options[:iface_type] != :private_network ||
                      options.fetch(:tunnel_type, nil)
              @logger.debug "Searching for network with options #{options}"

              # should fix other methods so this doesn't have to be instance var
              @options = options

              # Get a list of all (active and inactive) Libvirt networks. This
              # list is used throughout this class and should be easier to
              # process than Libvirt API calls.
              @available_networks = libvirt_networks(env[:machine].provider.driver)

              current_network = @available_networks.detect { |network| network[:name] == @options[:network_name] }

              # Prepare a hash describing network for this specific interface.
              @interface_network = {
                name:             nil,
                ip_address:       nil,
                netmask:          @options[:netmask],
                network_address:  nil,
                bridge_name:      nil,
                domain_name:      nil,
                ipv6_address:     options[:ipv6_address] || nil,
                ipv6_prefix:      options[:ipv6_prefix] || nil,
                created:          current_network.nil? ? false : true,
                active:           current_network.nil? ? false : current_network[:active],
                autostart:        options[:autostart] || false,
                guest_ipv6:       @options[:guest_ipv6] || 'yes',
                libvirt_network:  current_network.nil? ? nil : current_network[:libvirt_network]
              }

              if @options[:ip]
                handle_ip_option(env)
              elsif @options[:type].to_s == 'dhcp'
                handle_dhcp_private_network(env)
              elsif @options[:network_name]
                handle_network_name_option(env)
              else
                raise Errors::CreateNetworkError, error_message: @options
              end

              autostart_network if @interface_network[:autostart]
              activate_network unless @interface_network[:active]
            end
          end

          @app.call(env)
        end

        private

        def lookup_network_by_ip(ip)
          @logger.debug "looking up network with ip == #{ip}"
          @available_networks.find { |network| network[:network_address] == ip }
        end

        # Return hash of network for specified name, or nil if not found.
        def lookup_network_by_name(network_name)
          @logger.debug "looking up network named #{network_name}"
          @available_networks.find { |network| network[:name] == network_name }
        end

        # Return hash of network for specified bridge, or nil if not found.
        def lookup_bridge_by_name(bridge_name)
          @logger.debug "looking up bridge named #{bridge_name}"
          @available_networks.find { |network| network[:bridge_name] == bridge_name }
        end

        # Throw an error if dhcp setting for an existing network does not
        # match what was configured in the vagrantfile
        # since we always enable dhcp for the management network
        # this ensures we won't start a vm vagrant can't reach
        # Allow the situation where DHCP is not requested (:libvirt__dhcp_enabled == false)
        # but where it is enabled on the virtual network
        def verify_dhcp
          if @interface_network[:dhcp_enabled] == true && @options[:dhcp_enabled] == false
            raise Errors::DHCPMismatch,
                  network_name: @interface_network[:name],
                  requested: @options[:dhcp_enabled] ? 'enabled' : 'disabled'
          end
        end

        # Handle only situations, when ip is specified. Variables @options and
        # @available_networks should be filled before calling this function.
        def handle_ip_option(env)
          return unless @options[:ip]
          net_address = nil

          unless @options[:forward_mode] == 'veryisolated'
            net_address = network_address(@options[:ip], @options[:netmask])

            # Set IP address of network (actually bridge). It will be used as
            # gateway address for machines connected to this network.
            @interface_network[:ip_address] = get_host_ip_addr(net_address)
          end

          @interface_network[:network_address] = net_address

          # if network is veryisolated, search by name
          network = if @options[:libvirt__forward_mode] == 'veryisolated'
                      lookup_network_by_name(@options[:network_name])
                    elsif net_address
                      # otherwise, search by ip (if set)
                      lookup_network_by_ip(net_address)
                    else
                      # leaving this here to mimic prior behavior. If we get
                      # here, something's probably broken.
                      lookup_network_by_name(@options[:network_name])
                    end
          @interface_network = network if network

          verify_dhcp if @interface_network[:created]

          if @options[:network_name]
            @logger.debug 'Checking that network name does not clash with ip'
            if @interface_network[:created]
              # Just check for mismatch error here - if name and ip from
              # config match together.
              if @options[:network_name] != @interface_network[:name] and @qemu_use_agent == false
                raise Errors::NetworkNameAndAddressMismatch,
                      ip_address:   @options[:ip],
                      network_name: @options[:network_name]
              end
            else
              # Network is not created, but name is set. We need to check,
              # whether network name from config doesn't already exist.
              if lookup_network_by_name @options[:network_name]
                raise Errors::NetworkNameAndAddressMismatch,
                      ip_address:   @options[:ip],
                      network_name: @options[:network_name]
              end

              # Network with 'name' doesn't exist. Set it as name for new
              # network.
              @interface_network[:name] = @options[:network_name]
            end
          end

          # Do we need to create new network?
          unless @interface_network[:created]

            # TODO: stop after some loops. Don't create infinite loops.

            # Is name for new network set? If not, generate a unique one.
            count = 0
            while @interface_network[:name].nil?
              @logger.debug 'generating name for network'

              # Generate a network name.
              network_name = env[:root_path].basename.to_s.dup
              network_name << count.to_s
              count += 1

              # Check if network name is unique.
              next if lookup_network_by_name(network_name)

              @interface_network[:name] = network_name
            end

            # Generate a unique name for network bridge.
            @interface_network[:bridge_name] = generate_bridge_name

            # Create a private network.
            create_private_network(env)
            write_created_network(env)
          else
            write_created_network(env) unless @options[:always_destroy] == false
          end
        end

        # Handle network_name option, if ip was not specified. Variables
        # @options and @available_networks should be filled before calling this
        # function.
        def handle_network_name_option(env)
          return if @options[:ip] || \
                    !@options[:network_name] || \
                    !@options[:libvirt__forward_mode] == 'veryisolated'

          network = lookup_network_by_name(@options[:network_name])
          @interface_network = network if network

          if @options[:libvirt__forward_mode] == 'veryisolated'
            # if this interface has a network address, something's wrong.
            if @interface_network[:network_address]
              raise Errors::NetworkNotAvailableError,
                    network_name: @options[:network_name]
            end
          else
            if !@interface_network
              raise Errors::NetworkNotAvailableError,
                    network_name: @options[:network_name]
            else
              verify_dhcp
            end
          end

          # Do we need to create new network?
          unless @interface_network[:created]
            @interface_network[:name] = @options[:network_name]
            @interface_network[:ip_address] ||= @options[:host_ip]

            # Generate a unique name for network bridge.
            @interface_network[:bridge_name] = generate_bridge_name

            # Create a private network.
            create_private_network(env)
            write_created_network(env)
          else
            write_created_network(env) unless @options[:always_destroy] == false
          end
        end

        def handle_dhcp_private_network(env)
          net_address = @options[:libvirt__network_address]
          net_address = '172.28.128.0' unless net_address
          network = lookup_network_by_ip(net_address)

          @interface_network = network if network

          # Do we need to create new network?
          unless @interface_network[:created]
            @interface_network[:name] = @options[:network_name] ?
                                        @options[:network_name] :
                                        'vagrant-private-dhcp'
            @interface_network[:network_address] = net_address

            # Set IP address of network (actually bridge). It will be used as
            # gateway address for machines connected to this network.
            @interface_network[:ip_address] = get_host_ip_addr(net_address)

            # Generate a unique name for network bridge.
            @interface_network[:bridge_name] = generate_bridge_name

            # Create a private network.
            create_private_network(env)
            write_created_network(env)
          else
            write_created_network(env) unless @options[:always_destroy] == false
          end
        end

        # Return provided address or first address of network otherwise
        def get_host_ip_addr(network)
          @options[:host_ip] ? IPAddr.new(@options[:host_ip]) : IPAddr.new(network).succ
        end

        # Return the first available virbr interface name
        def generate_bridge_name
          @logger.debug 'generating name for bridge'
          count = 0
          while lookup_bridge_by_name(bridge_name = "virbr#{count}")
            count += 1
          end
          @logger.debug "found available bridge name #{bridge_name}"
          bridge_name
        end

        def create_private_network(env)
          @network_name = @interface_network[:name]
          @network_bridge_name = @interface_network[:bridge_name]
          @network_address = @interface_network[:ip_address]
          @network_netmask = @interface_network[:netmask]
          @network_mtu = Integer(@options[:mtu]) if @options[:mtu]

          @guest_ipv6 = @interface_network[:guest_ipv6]

          @network_ipv6_address = @interface_network[:ipv6_address]
          @network_ipv6_prefix = @interface_network[:ipv6_prefix]

          @network_bridge_stp = @options[:bridge_stp].nil? || @options[:bridge_stp] ? 'on' : 'off'
          @network_bridge_delay = @options[:bridge_delay] ? @options[:bridge_delay] : 0

          @network_forward_mode = @options[:forward_mode]
          if @options[:forward_device]
            @network_forward_device = @options[:forward_device]
          end

          if @options[:dhcp_enabled]
            # Find out DHCP addresses pool range.
            network_address = "#{@interface_network[:network_address]}/#{(@interface_network[:netmask]).to_s}"
            net = @interface_network[:network_address] ? IPAddr.new(network_address) : nil

            # First is address of network, second is gateway (by default).
            # So start the range two addresses after network address by default.
            # TODO: Detect if this IP is not set on the interface.
            start_address = @options[:dhcp_start] || net.to_range.begin.succ

            # Default to last possible address. (Stop address must not be broadcast address.)
            stop_address = @options[:dhcp_stop] || (net.to_range.end & IPAddr.new('255.255.255.254'))

            @network_dhcp_enabled = true
            @network_dhcp_bootp_file = @options[:dhcp_bootp_file]
            @network_dhcp_bootp_server = @options[:dhcp_bootp_server]
            @network_range_start = start_address
            @network_range_stop = stop_address
          else
            @network_dhcp_enabled = false
          end

          if @options[:tftp_root]
            @tftp_root = @options[:tftp_root]
          end

          @network_domain_name = @options[:domain_name]

          begin
            xml = to_xml('private_network')
            @logger.debug {
              "Creating private network with XML:\n#{xml}"
            }
            @interface_network[:libvirt_network] = \
              @libvirt_client.define_network_xml(xml)
            @logger.debug 'created network'
          rescue => e
            raise Errors::CreateNetworkError, error_message: e.message
          end
        end

        def write_created_network(env)
          created_networks_file = env[:machine].data_dir + 'created_networks'

          message = 'Saving information about created network ' \
            "#{@interface_network[:name]}, UUID=#{@interface_network[:libvirt_network].uuid} " \
            "to file #{created_networks_file}."
          @logger.info(message)

          File.open(created_networks_file, 'a') do |file|
            file.puts @interface_network[:libvirt_network].uuid
          end
        end

        def autostart_network
          @interface_network[:libvirt_network].autostart = true
        rescue => e
          raise Errors::AutostartNetworkError, error_message: e.message
        end

        def activate_network
          @interface_network[:libvirt_network].create
        rescue => e
          raise Errors::ActivateNetworkError, error_message: e.message
        end
      end
    end
  end
end
