require 'metadata/util/md5deep'
require 'util/miq-xml'
require 'ostruct'
require 'manageiq-password'
require 'metadata/util/win32/Win32Accounts'
require 'metadata/util/win32/Win32Software'
require 'metadata/util/win32/Win32Services'
require 'metadata/util/win32/Win32System'
require 'metadata/util/win32/Win32EventLog'
require 'metadata/linux/LinuxUsers'
require 'metadata/linux/LinuxPackages'
require 'metadata/linux/LinuxInitProcs'
require 'metadata/linux/LinuxSystemd'
require 'metadata/linux/LinuxOSInfo'
require 'metadata/ScanProfile/VmScanProfiles'
require 'VMwareWebService/MiqVim'
require 'OpenStackExtract/MiqOpenStackVm/MiqOpenStackImage'
require 'OpenStackExtract/MiqOpenStackVm/MiqOpenStackInstance'

class MIQExtract
  attr_reader :systemFsMsg, :systemFs, :vm

  def initialize(filename, ost = nil) # TODO: Always pass in MiqVm
    ost ||= OpenStruct.new
    @ost = ost
    @dataDir = ost.config.try(:dataDir)
    ost.scanData = {} if ost.scanData.nil?
    @xml_class = ost.xml_class.nil? ? XmlHash::Document : ost.xml_class

    # TODO: Should all be subclasses of MiqVm.
    #       Going forward, we should only pass in an MiqVm - so the "else" will be removed.
    if filename.respond_to?(:rootTrees)
      @externalMount = true
      @target = filename
      @configFile = filename.respond_to?(:vmConfigFile) ? @target.vmConfigFile : nil

      $log.info "MIQExtract using config file: [#{@configFile}]  settings: [#{ManageIQ::Password.sanitize_string(ost.scanData.inspect)}]"
    else
      @externalMount = false
      @configFile = filename.gsub(/^"/, "").gsub(/"$/, "")

      $log.info "MIQExtract using config file: [#{@configFile}]  settings: [#{ManageIQ::Password.sanitize_string(ost.scanData.inspect)}]"
      ost.openParent = true if ost.scanData.fetch_path('snapshot', 'use_existing') == true
      ost.force = false if ost.scanData.fetch_path('snapshot', 'forceFleeceDefault') == false
      ost.snapshotDescription = ost.scanData.fetch_path('snapshot', 'description') if ost.scanData.fetch_path('snapshot', 'description')
      ost.snapshot_create_free_space = ost.scanData.fetch_path('snapshot', 'create_free_percent') || 100
      ost.snapshot_remove_free_space = ost.scanData.fetch_path('snapshot', 'remove_free_percent') || 100
      @target = MiqVm.new(@configFile, ost)
    end

    # Set the system fs handle
    begin
      st = Time.now
      $log.info "Loading disk files for VM [#{@configFile}]"
      @systemFs = @target.rootTrees[0]
      if @systemFs.nil?
        raise MiqException::MiqVmMountError, "No root filesystem found." if @target.diskInitErrors.empty?

        err_msg = ''
        @target.diskInitErrors.each do |disk, err|
          err = "#{err} - #{disk}" unless err.include?(disk)
          err_msg += "#{err}\n"
        end
        raise err_msg.chomp
      end

      @systemFsMsg = "OS:[#{@systemFs.guestOS}] found on VM [#{@configFile}].  Loaded in [#{Time.now - st}] seconds"
      $log.info @systemFsMsg
    rescue => err
      @systemFsMsg = "Unable to mount filesystem.  Reason:[#{err}]"
      $log.error "#{@systemFsMsg} for VM [#{@configFile}]"
      log_level = err.kind_of?(MiqException::Error) ? :debug : :error
      err.backtrace.each { |bt| $log.send(log_level, "MIQExtract.new #{bt}") }
      close
    end

    # Load Scan Profiles
    @scanProfiles = VmScanProfiles.new(ost.scanData['vmScanProfiles'], VmScanProfiles::SCAN_ITEM_CATEGORIES => ost.category, :xml_class => @xml_class)
  end

  def categories
    @scanProfiles.get_categories
  end

  def split_registry(path)
    y = path.tr("\\", "/").split("/")
    return y[0].downcase, y[1..-1].join("/")
  end

  def getProfileData(&blk)
    # First check for registry keys
    if @systemFs.guestOS == "Windows"
      reg_filters = @scanProfiles.get_registry_filters

      if reg_filters
        st = Time.now
        $log.info "Scanning [Profile-Registry] information."
        yield({:msg => 'Scanning Profile-Registry'}) if block_given?

        filters = []
        Array.wrap(reg_filters[:HKCU]).each { |f| filters << {:key => split_registry(f['key']).join('/'), :depth => f['depth']} }
        @scanProfiles.parse_data(@target, RemoteRegistry.new(@systemFs, @xml_class).loadCurrentUser(filters)) unless filters.empty?

        filters = {}
        reg_filters[:HKLM].each do |f|
          hive, path = split_registry(f["key"])
          filters[hive.downcase] = [] if filters[hive.downcase].nil?
          filters[hive.downcase] << path
        end unless reg_filters[:HKLM].nil?

        filters.each_pair do |k, v|
          regHnd = RemoteRegistry.new(@systemFs, @xml_class)
          xml = regHnd.loadHive(k, v)
          @scanProfiles.parse_data(@target, xml)
        end unless filters.empty?
        $log.info "Scanning [Profile-Registry] information ran for [#{Time.now - st}] seconds."
      end
    end

    # Pass in the MiqVm handle to do file parsing
    @scanProfiles.parse_data(@target, nil, &blk)

    @scanProfiles.to_xml
  end

  def extract(category, &blk)
    xml = nil
    Array.wrap(category.presence).each do |c|
      c = c.downcase
      xml = case c
            when "accounts" then getAccounts(c)
            when "services" then getServices(c)
            when "software" then getSoftware(c)
            when "system"   then getSystem(c)
            when "ntevents" then getEventLogs(c)
            when "vmconfig" then getVMConfig(c)
            # vmEvents are added to the blackbox externally, not extracted from the vm disks.
            when "vmevents" then return nil
            when "profiles" then getProfileData(&blk)
            else
              $log.warn "Warning: Category not processed [#{c}]"
              nil
            end

      # Write XML to data directory for debugging
      # File.open(File.join(@dataDir, "scan_#{c}.xml"),"w") {|f| xml.write(f,0)} if xml rescue nil
    end
  rescue => err
    $log.error "MIQExtract.extract #{err}"
    log_level = err.kind_of?(MiqException::Error) ? :debug : :error
    err.backtrace.each { |bt| $log.send(log_level, "MIQExtract.extract #{bt}") }
    raise err
  ensure
    return xml
  end

  def getVMConfig(_c)
    # Get VM config in XML format
    config_xml = @target.vmConfig.toXML(true, @target)

    begin
      # Log snapshot data for diagnostic purposes
      config_xml.find_each("//vm/snapshots") do |s|
        formattedXml = ""
        s.write(formattedXml, 0)

        # Dump the formated snapshot xml section
        formattedXml.split("\n").each { |l| $log.debug l }

        # Log the high level snapshot details
        $log.info "Snapshot overview: Count:[#{s.attributes['numsnapshots']}]  Current:[#{s.attributes['current']}]"

        # Log each child snapshot element
        s.find_each("child::snapshot") { |ss| $log.info "Snapshot details:  id:[#{ss.attributes['uid']}]  Name:[#{ss.attributes['displayname']}]" }
      end
    rescue # => e
      # $log.error e
    end

    if config_xml.class != @xml_class
      config_xml = @xml_class.load(config_xml.to_s)
    end

    config_xml
  end

  def xml_doc_node(category)
    doc = @xml_class.createDoc(:miq)
    node = doc.root.add_element(category)
    return doc, node
  end

  def getAccounts(c)
    return if @systemFs.nil?

    doc, node = xml_doc_node(:accounts)
    #   doc.root.attributes["xsi:schemaLocation"] += " accounts.xsd"

    case @systemFs.guestOS
    when "Windows"
      MiqWin32::Accounts.new(c, @systemFs).to_xml(node)
    when "Linux"
      MiqLinux::Users.new(@systemFs).to_xml(node)
    end

    doc
  end

  def getServices(c)
    return if @systemFs.nil?

    doc, node = xml_doc_node(:services)
    #   doc.root.attributes["xsi:schemaLocation"] += " services.xsd"

    case @systemFs.guestOS
    when "Windows"
      MiqWin32::Services.new(c, @systemFs).to_xml(node)
    when "Linux"
      MiqLinux::Systemd.new(@systemFs).toXml(node) if MiqLinux::Systemd.detected?(@systemFs)
      MiqLinux::InitProcs.new(@systemFs).toXml(node)
    end

    doc
  end

  def getSystem(c)
    return if @systemFs.nil?

    doc, node = xml_doc_node(:system)
    #   doc.root.attributes["xsi:schemaLocation"] += " system.xsd"

    case @systemFs.guestOS
    when "Windows"
      MiqWin32::System.new(c, @systemFs).to_xml(node)
    when "Linux"
      MiqLinux::OSInfo.new(@systemFs).toXml(node)
    end

    doc
  end

  def getSoftware(c)
    return if @systemFs.nil?

    doc, node = xml_doc_node(:software)
    #   doc.root.attributes["xsi:schemaLocation"] += " software.xsd"

    case @systemFs.guestOS
    when "Windows"
      MiqWin32::Software.new(c, @systemFs).to_xml(node)
    when "Linux"
      MiqLinux::Packages.new(@systemFs).toXml(node)
    end

    doc
  end

  def getEventLogs(_c)
    ntevent = Win32EventLog.new(@systemFs)
    ntevent.readAllLogs(Win32EventLog::SYSTEM_LOGS)
  end

  def scanMD5deep
    # Produce file system XML with MD5 info
    $log.debug "Starting Filesystem scan"
    # scanPath = "c:/program files"
    scanPath = "c:/program files/Java/jdk1.5.0_12/jre/lib"
    md5 = MD5deep.new(@systemFs, 'versioninfo' => true, 'imports' => true)
    md5.scan(scanPath, scanPath)
  end

  def getBaseConfigName
    File.join(File.dirname(@configFile), File.basename(@configFile, ".*"))
  end

  def close
    return if @externalMount
    # Call Unmount command if drive was succesfully mounted
    unless @target.nil?
      $log.debug "Unmounting..."
      begin
        @target.unmount
        $log.debug "Unmounting complete"
      rescue => err
        $log.error "Error during disk unmounting for VM:[#{@configFile}]"
        $log.debug err.backtrace.join("\n")
      ensure
        @ost.miqVim.disconnect if @ost.miqVim
        unmount_storage(@mount) unless @mount.blank?
        set_process_permissions(:reset)
      end
    end
  end  # close
end  # MIQExtract class