require 'fileutils'
require 'json'
require 'uri'
require 'vagrant/util/subprocess'
require 'vagrant/util/downloader'
require 'vagrant/box'
require 'vagrant/box_metadata'

module VagrantMutate
  class BoxLoader
    def initialize(env)
      @env = env
      @logger = Log4r::Logger.new('vagrant::mutate')
      @tmp_files = []
    end

    def prepare_for_output(name, provider_name, version)
      @logger.info "Preparing #{name} for output as #{provider_name} with version #{version}."
      safe_name = sanitize_name(name)
      dir = create_output_dir(safe_name, provider_name, version)
      box = create_box(provider_name, name, version, dir)

      if box.supported_output
        return box
      else
        fail Errors::ProviderNotSupported, provider: provider_name, direction: 'output'
      end
    end

    def load(box_arg, provider_name, input_version)
      if box_arg =~ /:\/\//
        box = load_from_url(box_arg)
      elsif File.file?(box_arg)
        box = load_from_file(box_arg)
      else
        box = load_from_boxes_path(box_arg, provider_name, input_version)
      end

      if box.supported_input
        return box
      else
        fail Errors::ProviderNotSupported, provider: box.provider_name, direction: 'input'
      end
    end

    def load_from_url(url)
      @logger.info "Loading box from url #{url}"

      # test that we have a valid url
      url = URI(url)
      unless url.scheme and url.host and url.path
        fail Errors::URLError, url: url
      end

      # extract the name of the box from the url
      # if it ends in .box remove that extension
      # if not just remove leading slash
      name = nil
      if url.path =~ /([-.\w]+).box$/
        name = Regexp.last_match[1]
      else
        name = url.path.sub(/^\//, '')
      end
      if name.empty?
        fail Errors::URLError, url: url
      end

      # Extract the version of the box from the URL
      if url.path =~ /\/([\d.]+)\//
        version = Regexp.last_match[1]
        @logger.info "Pulled version from URL (#{version})"
      else
        version = '0'
        @logger.info "No version found in URL, assuming '0'"
      end

      # using same path as in vagrants box add action
      download_path = File.join(@env.tmp_path, 'box' + Digest::SHA1.hexdigest(url.to_s))
      @tmp_files << download_path

      # if this fails it will raise an error and we'll quit
      @env.ui.info "Downloading box #{name} from #{url}"
      downloader = Vagrant::Util::Downloader.new(url, download_path,  ui: @env.ui)
      downloader.download!

      dir = unpack(download_path)
      provider_name = determine_provider(dir)

      create_box(provider_name, name, version, dir)
    end

    def load_from_file(file)
      @logger.info "Loading box from file #{file}"
      name = File.basename(file, File.extname(file))

      dir = unpack(file)
      provider_name = determine_provider(dir)
      version = determine_version(dir)

      create_box(provider_name, name, version, dir)
    end

    def load_from_boxes_path(name, provider_name, input_version)
      @logger.info "Loading box #{name} from vagrants box path using provider #{provider_name} and version #{input_version}."
      safe_name = sanitize_name(name)
      if provider_name
        @logger.info "Checking directory for provider #{provider_name}."
        if input_version
          @logger.info 'Input version provided, using it.'
          version = input_version
        else
          @logger.info 'No version provided, getting it.'
          version = get_version(safe_name)
          @logger.info "Version = #{version}"
        end
        dir = verify_input_dir(provider_name, safe_name, version)
      else
        @logger.info 'Working out provider, version and directory...'
        provider_name, version, dir = find_input_dir(safe_name)
      end
      @logger.info "Creating #{name} box using provider #{provider_name} with version #{version} in #{dir}."
      create_box(provider_name, name, version, dir)
    end

    def cleanup
      unless @tmp_files.empty?
        @env.ui.info 'Cleaning up temporary files.'
        @tmp_files.each do |f|
          @logger.info "Deleting #{f}"
          FileUtils.remove_entry_secure(f)
        end
      end
    end

    private

    def create_box(provider_name, name, version, dir)
      @logger.info "Creating box #{name} with provider #{provider_name} and version #{version} in #{dir}"
      case provider_name
      when 'bhyve'
        require_relative 'box/bhyve'
        Box::Bhyve.new(@env, name, version, dir)
      when 'kvm'
        require_relative 'box/kvm'
        Box::Kvm.new(@env, name, version, dir)
      when 'libvirt'
        require_relative 'box/libvirt'
        Box::Libvirt.new(@env, name, version, dir)
      when 'virtualbox'
        require_relative 'box/virtualbox'
        Box::Virtualbox.new(@env, name, version, dir)
      else
        fail Errors::ProviderNotSupported, provider: provider_name, direction: 'input or output'
      end
    end

    def create_output_dir(name, provider_name, version)
      # e.g. $HOME/.vagrant.d/boxes/fedora-19/0/libvirt
      @logger.info "Attempting to create output dir for #{name} with version #{version} and provider #{provider_name}."
      out_dir = File.join(@env.boxes_path, name, version, provider_name)
      @logger.info "Creating out_dir #{out_dir}."
      begin
        FileUtils.mkdir_p(out_dir)
      rescue => e
        raise Errors::CreateBoxDirFailed, error_message: e.message
      end
      @logger.info "Created output directory #{out_dir}"
      out_dir
    end

    def unpack(file)
      @env.ui.info 'Extracting box file to a temporary directory.'
      unless File.exist? file
        fail Errors::BoxNotFound, box: file
      end
      tmp_dir = Dir.mktmpdir(nil, @env.tmp_path)
      @tmp_files << tmp_dir
      result = Vagrant::Util::Subprocess.execute(
       'bsdtar', '-v', '-x', '-m', '-C', tmp_dir.to_s, '-f', file)
      if result.exit_code != 0
        fail Errors::ExtractBoxFailed, error_message: result.stderr.to_s
      end
      @logger.info "Unpacked box to #{tmp_dir}"
      tmp_dir
    end

    def determine_provider(dir)
      metadata_file = File.join(dir, 'metadata.json')
      if File.exist? metadata_file
        begin
          metadata = JSON.load(File.new(metadata_file, 'r'))
        rescue => e
          raise Errors::LoadMetadataFailed, error_message: e.message
        end
        @logger.info "Determined input provider is #{metadata['provider']}"
        return metadata['provider']
      else
        @logger.info 'No metadata found, so assuming input provider is virtualbox'
        return 'virtualbox'
      end
    end

    def determine_version(dir)
      metadata_file = File.join(dir, 'metadata.json')
      if File.exist? metadata_file
        begin
          metadata = JSON.load(File.new(metadata_file, 'r'))
        rescue => e
          raise Errors::LoadMetadataFailed, error_message: e.message
        end
        # Handle single or multiple versions
        if metadata['versions'].nil?
          @logger.info 'No versions provided by metadata, asuming version 0'
          version = '0'
        elsif metadata['versions'].length > 1
          metadata['versions'].each do |metadata_version|
            @logger.info 'Itterating available metadata versions for active version.'
            next unless metadata_version['status'] == 'active'
            version = metadata_version['version']
          end
        else
          @logger.info 'Only one metadata version, grabbing version.'
          version = metadata['versions'][0]['version']
        end
        @logger.info "Determined input version is #{version}"
        return version
      else
        @logger.info 'No metadata found, so assuming version is 0'
        return '0'
      end
    end

    def verify_input_dir(provider_name, name, version)
      input_dir = File.join(@env.boxes_path, name, version, provider_name)
      if File.directory?(input_dir)
        @logger.info "Found input directory #{input_dir}"
        return input_dir
      else
        fail Errors::BoxNotFound, box: input_dir
      end
    end

    def find_input_dir(name)
      @logger.info "Looking for input dir for box #{name}."
      version = get_version(name)
      box_parent_dir = File.join(@env.boxes_path, name, version)

      if Dir.exist?(box_parent_dir)
        providers = Dir.entries(box_parent_dir).reject { |entry| entry =~ /^\./ }
        @logger.info "Found potential providers #{providers}"
      else
        providers = []
      end

      case
      when providers.length < 1
        fail Errors::BoxNotFound, box: name
      when providers.length > 1
        fail Errors::TooManyBoxesFound, box: name
      else
        provider_name = providers.first
        input_dir = File.join(box_parent_dir, provider_name)
        @logger.info "Found source for box #{name} from provider #{provider_name} with version #{version} at #{input_dir}"
        return provider_name, version, input_dir
      end
    end

    def get_version(name)
      # Get a list of directories for this box
      @logger.info "Getting versions for #{name}."

      box_dir = File.join(@env.boxes_path, name, '*')
      possible_versions = Dir.glob(box_dir).select { |f| File.directory? f }.map { |x| x.split('/').last }

      @logger.info "Possible_versions = #{possible_versions.inspect}"

      if possible_versions.length > 1
        @logger.info 'Got multiple possible versions, selecting max value'
        version = possible_versions.max
      elsif possible_versions.length == 1
        @logger.info 'Got a single version, so returning it'
        version = possible_versions.first
      else
        fail Errors::BoxNotFound, box: name
      end

      @logger.info "Found version #{version}"
      version
    end

    def sanitize_name(name)
      if name =~ /\//
        @logger.info 'Replacing / with -VAGRANTSLASH-.'
        name = name.dup
        name.gsub!('/', '-VAGRANTSLASH-')
        @logger.info "New name = #{name}."
      end
      name
    end
  end
end
