require 'net/http'
require 'tempfile'
require 'tmpdir'
require 'uri'

# Generates an iPXE ISO hybrid image
#
# requires syslinux, ipxe/ipxe-bootimgs, genisoimage, isohybrid
class ForemanBootdisk::ISOGenerator
  def self.generate_full_host(host, opts = {}, &block)
    raise ::Foreman::Exception.new(N_('Host is not in build mode, so the template cannot be rendered')) unless host.build?

    tmpl = host.send(:generate_pxe_template)
    raise ::Foreman::Exception.new(N_('Unable to generate disk template: %s'), host.errors.full_messages.to_sentence) if tmpl == false

    # pxe_files and filename conversion is utterly bizarre
    # aim to convert filenames to something usable under ISO 9660, update the template to match
    # and then still ensure that the fetch() process stores them under the same name
    files = host.operatingsystem.pxe_files(host.medium, host.architecture, host)
    files.map! do |bootfile_info|
      bootfile_info.map do |f|
        suffix = f[1].split('/').last
        iso_suffix = iso9660_filename(suffix)
        iso_f0 = iso9660_filename(f[0].to_s) + '_' + iso_suffix
        tmpl.gsub!(f[0].to_s + '-' + suffix, iso_f0)
        Rails.logger.debug("Boot file #{iso_f0}, source #{f[1]}")
        [iso_f0, f[1]]
      end
    end

    generate(opts.merge(:isolinux => tmpl, :files => files), &block)
  end

  def self.generate(opts = {}, &block)
    opts[:isolinux] = <<-EOS if opts[:isolinux].nil? && opts[:ipxe]
      default ipxe
      label ipxe
      kernel /ipxe
      initrd /script
    EOS

    Dir.mktmpdir('bootdisk') do |wd|
      Dir.mkdir(File.join(wd, 'build'))

      if opts[:isolinux]
        unless File.exists?(File.join(Setting[:bootdisk_syslinux_dir], 'isolinux.bin'))
          raise ::Foreman::Exception.new(N_("Please ensure the ipxe-bootimgs and syslinux packages are installed."))
        end
        FileUtils.cp(File.join(Setting[:bootdisk_syslinux_dir], 'isolinux.bin'), File.join(wd, 'build', 'isolinux.bin'))
        if File.exist?(File.join(Setting[:bootdisk_syslinux_dir], 'ldlinux.c32'))
          FileUtils.cp(File.join(Setting[:bootdisk_syslinux_dir], 'ldlinux.c32'), File.join(wd, 'build', 'ldlinux.c32'))
        end
        File.open(File.join(wd, 'build', 'isolinux.cfg'), 'w') do |file|
          file.write(opts[:isolinux])
        end
      end

      if opts[:ipxe]
        unless File.exists?(File.join(Setting[:bootdisk_ipxe_dir], 'ipxe.lkrn'))
          raise ::Foreman::Exception.new(N_("Please ensure the ipxe-bootimgs and syslinux packages are installed."))
        end
        FileUtils.cp(File.join(Setting[:bootdisk_ipxe_dir], 'ipxe.lkrn'), File.join(wd, 'build', 'ipxe'))
        File.open(File.join(wd, 'build', 'script'), 'w') { |file| file.write(opts[:ipxe]) }
      end

      if opts[:files]
        opts[:files].each do |bootfile_info|
          for file, source in bootfile_info do
            fetch(File.join(wd, 'build', file), source)
          end
        end if opts[:files].respond_to? :each
      end

      iso = if opts[:dir]
              Tempfile.new(['bootdisk', '.iso'], opts[:dir]).path
            else
              File.join(wd, 'output.iso')
            end
      unless system("#{Setting[:bootdisk_mkiso_command]} -o #{iso} -iso-level 2 -b isolinux.bin -c boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table #{File.join(wd, 'build')}")
        raise ::Foreman::Exception.new(N_("ISO build failed"))
      end

      # Make the ISO bootable as a HDD/USB disk too
      unless system("isohybrid", iso)
        raise ::Foreman::Exception.new(N_("ISO hybrid conversion failed"))
      end

      yield iso
    end
  end

  def self.token_expiry(host)
    expiry = host.token.try(:expires)
    return '' if Setting[:token_duration] == 0 || expiry.blank?
    '_' + expiry.strftime('%Y%m%d_%H%M')
  end

  private

  def self.fetch(path, uri)
    dir = File.dirname(path)
    FileUtils.mkdir_p(dir) unless File.exist?(dir)

    use_cache = !!Setting[:bootdisk_cache_media]
    write_cache = false
    File.open(path, 'w') do |file|
      file.binmode

      if use_cache && !(contents = Rails.cache.fetch(uri, :raw => true)).nil?
        Rails.logger.info("Retrieved #{uri} from local cache (use foreman-rake tmp:cache:clear to empty)")
        file.write(contents)
      else
        Rails.logger.info("Fetching #{uri}")
        write_cache = use_cache
        uri = URI(uri)
        Net::HTTP.start(uri.host, uri.port) do |http|
          request = Net::HTTP::Get.new uri.request_uri

          http.request request do |response|
            response.read_body do |chunk|
              file.write chunk
            end
          end
        end
      end
    end

    if write_cache
      Rails.logger.debug("Caching contents of #{uri}")
      Rails.cache.write(uri, File.read(path), :raw => true)
    end
  end

  # isolinux supports up to ISO 9660 level 2 filenames
  def self.iso9660_filename(name)
    dir  = File.dirname(name)
    file = File.basename(name).upcase.tr_s('^A-Z0-9_', '_')[0..30]
    dir == '.' ? file : File.join(dir.upcase.tr_s('^A-Z0-9_', '_')[0..30], file)
  end
end