require 'auto_network/pool'
require 'auto_network/pool_storage'
require 'ipaddr'

module AutoNetwork
  # The `PoolManager` class manages the mapping between providers and pools of
  # IP addresses. Each `PoolManager` instance is backed by a file that persists
  # state and attempts to prevent race conditions between multiple Vagrant
  # processes.
  #
  # Once created, `PoolManager` instances proxy the public interface of the
  # {AutoNetwork::Pool} instances they manage. New pools will be allocated as
  # needed and all pool operations are wrapped in transactions that ensure
  # state is synced to the file system.
  #
  # @see AutoNetwork::PoolStorage The object used to implement file-based
  #   persistance for this class.
  # @see AutoNetwork::Pool The objects managed by this class.
  #
  # @since 1.0.0
  class PoolManager
    # Create a new `PoolManager` instance with persistent storage.
    #
    # @param path [String, Pathname] the location at which to persist the state
    #   of `AutoNetwork` pools.
    def initialize(path)
      # Ensure newly created files start with a skeleton data structure.
      AutoNetwork::PoolStorage.init(path) unless File.file?(path)
      # Enable thread safety an atomic file updates on the underlying
      # YAML::Store instance.
      @pool_storage = AutoNetwork::PoolStorage.new(path, true)
      @pool_storage.ultra_safe = true
    end

    # Looks up the pool associated with the provider for a given machine and
    # sets up a transaction where the state of the pool can be safely inspected
    # or modified. If a pool does not exist for the machine provider, one will
    # automatically be created.
    #
    # @param machine [Vagrant::Machine]
    # @param read_only [Boolean] whether to create a read_only transaction.
    # @yieldparam pool [AutoNetwork::Pool]
    def with_pool_for(machine, read_only=false)
      @pool_storage.transaction(read_only) do
        pool = lookup_pool_for(machine)
        pool ||= generate_pool_for(machine)

        yield pool
      end
    end

    # (see AutoNetwork::Pool#request)
    #
    # @see AutoNetwork::Pool#request This method is a proxy for
    #   AutoNetwork::Pool#request
    def request(machine)
      with_pool_for(machine) do |pool|
        pool.request(machine)
      end
    end

    # (see AutoNetwork::Pool#release)
    #
    # @see AutoNetwork::Pool#release This method is a proxy for
    #   AutoNetwork::Pool#release
    def release(machine)
      with_pool_for(machine) do |pool|
        pool.release(machine)
      end
    end

    # (see AutoNetwork::Pool#address_for)
    #
    # @see AutoNetwork::Pool#address_for This method is a proxy for
    #   AutoNetwork::Pool#address_for
    def address_for(machine)
      with_pool_for(machine, read_only=true) do |pool|
        pool.address_for(machine)
      end
    end

    private

    # Retrieve the {AutoNetwork::Pool} assigned to the provider of a given
    # machine.
    #
    # @note This must be executed within a transaction.
    # @api private
    #
    # @param machine [Vagrant::Machine]
    # @return [AutoNetwork::Pool] the pool associated with the machine
    #   provider.
    # @return [nil] if no pool exists for the machine provider.
    def lookup_pool_for(machine)
      @pool_storage['pools'][machine.provider_name.to_s]
    end

    # Create an {AutoNetwork::Pool} assigned to the provider of a given
    # machine.
    #
    # @note This must be executed within a transaction.
    # @api private
    #
    # @param machine [Vagrant::Machine]
    # @return [AutoNetwork::Pool] the pool associated with the machine
    #   provider.
    def generate_pool_for(machine)
      if lookup_pool_for(machine).nil?
        @pool_storage['pools'][machine.provider_name.to_s] = AutoNetwork::Pool.new(next_pool_range)
      end

      lookup_pool_for(machine)
    end

    # Scan the list of allocated pools and determine the next usable address
    # range. Assumes all Pools use a "/24" address range and share the same
    # "/16" range.
    #
    # @note This must be executed within a transaction.
    # @api private
    #
    # @return [String] an IP range ending in "/24".
    def next_pool_range
      # Fetch the list of pools under management. Return nil if no pools exist.
      pools = @pool_storage.fetch('pools', nil)

      if pools.empty?
        # If no pools have been created, use the default range.
        AutoNetwork.default_pool
      else
        # Look up the highest "XX.XX.YY.XX/24" range in use.
        last_pool_range = IPAddr.new(pools.values.map{|v| v.network_range}.sort.last)
        # Increment "YY" by one to generate a new "/24" range.
        ((last_pool_range >> 8).succ << 8).to_s + '/24'
      end
    end
  end
end
