require 'rexml/document'
require 'cgi'
require 'digest'
require 'set'
require 'securerandom'

require_relative 'oms_common'
require_relative 'oms_configuration'
require_relative 'omslog'

class LinuxUpdates

    @@prev_hash = ""
    @@delimiter = "_"
    @@force_send_last_upload = Time.now
    @@os_details = nil

    MAJOR_MINOR_VERSION_REGEX = /([^\.]+)\.([^\.]+).*/
    OMS_ADMIN_FILE = "/etc/opt/microsoft/omsagent/conf/omsadmin.conf"
    SCX_RELEASE_FILE = "/etc/opt/microsoft/scx/conf/scx-release"
    SCHEDULE_NAME_VARIABLE = "SCHEDULE_NAME"
    APT_GET_START_DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"

    def getAgentId()
        return OMS::Configuration.agent_id
    end

    def getHostOSDetails()
        if(@@os_details.nil?)    
           if File.exist?(SCX_RELEASE_FILE) # If file exists
            File.open(SCX_RELEASE_FILE, "r") do |f| # Open file
			@@os_details={}
                f.each_line do |line|       # Split each line and
                    line.split(/\r?\n/).reject{ |l|  # reject all those line
                        !l.include? "=" }.map! {|s|    # which do not
                            s.split("=")}.map! {|key, value| # have "="; split
                                @@os_details[key] = value     # by "=" and add to ret.
                        }
                end
            end
            else
                OMS::Log.info_once("Could not find the file #{SCX_RELEASE_FILE}")
                @@os_details = {}
            end
        end

        return @@os_details
    end

    # This temporary fix is for version management for cache lookup.                        
    def getOSShortName(os_short_name = nil, os_version=nil)
        version = ""
        hostOSDetailsMap = getHostOSDetails()
        #os short name is not proper for oracle linux at /etc/opt/microsoft/scx/conf/scx-release. this is to return proper short name till scx fixes the issue.
        if hostOSDetailsMap.key?("OSFullName") && hostOSDetailsMap.key?("OSShortName")
            osFullName = hostOSDetailsMap["OSFullName"]
            osShortName = hostOSDetailsMap["OSShortName"]
            if osFullName.downcase.include?("oracle") && ! osShortName.downcase.include?("oracle")
                os_short_name = "Oracle"
            end
        end

        # match string of the form (1 or more non . chars)- followed by a . - (1 or more non . chars) - followed by anything
        if hostOSDetailsMap.key?("OSShortName")
            osName = (os_short_name.nil?) ? hostOSDetailsMap["OSShortName"].split("_")[0] : os_short_name.split("_")[0]
        else
            osName =  (os_short_name.nil?) ? hostOSDetailsMap["OSFullName"] : os_short_name.split("_")[0]
        end

        if (os_version.nil?)
            @os_major_version = hostOSDetailsMap["OSVersion"][MAJOR_MINOR_VERSION_REGEX, 1] unless hostOSDetailsMap["OSVersion"].nil?
            @os_minor_version = hostOSDetailsMap["OSVersion"][MAJOR_MINOR_VERSION_REGEX, 2] unless hostOSDetailsMap["OSVersion"].nil?
            @default_version = hostOSDetailsMap["OSVersion"]
        else
            @os_major_version = os_version[MAJOR_MINOR_VERSION_REGEX, 1] unless os_version.nil?
            @os_minor_version = os_version[MAJOR_MINOR_VERSION_REGEX, 2] unless os_version.nil?
            @default_version = os_version
        end 
        
        case osName
        when "Ubuntu"
            if @os_major_version == "12" || @os_major_version == "13"
                version = "12.04"
            elsif  @os_major_version == "14" || @os_major_version == "15"
                version = "14.04"
            elsif  @os_major_version == "16" || @os_major_version == "17"
                version = "16.04"
            else
                version = @default_version
            end
        when "CentOS"
            if @os_major_version == "5"
                version = "5.0" 
            elsif  @os_major_version == "6"
                version = "6.0"
            elsif  @os_major_version == "7"
                version = "7.0"
            elsif  @os_major_version == "8"
                version = "8.0"
            else
                version = @default_version
            end
        when "RHEL"
            if @os_major_version == "5"
                version = "5.0" 
            elsif  @os_major_version == "6"
                version = "6.0"
            elsif  @os_major_version == "7"
                version = "7.0"
            elsif  @os_major_version == "8"
                version = "8.0"
            else
                version = @default_version
            end
        when "Oracle"
            version = "6.0"
        when "SUSE"
            if @os_major_version == "11"
                version = "11.0" 
            elsif  @os_major_version == "12"
                version = "12.0"
            elsif  @os_major_version == "15"
                version = "15.0"
            else
                version = @default_version
            end
        else
            version = @default_version
        end

        return osName + @@delimiter + version
    end

    def self.prev_hash= (value)
        @@prev_hash = value
    end

    def isInstalledPackageInstanceXML(instanceXML)
        instanceXML.attributes["CLASSNAME"] == "MSFT_nxPackageResource"
    end

    def isAvailableUpdateInstanceXML(instanceXML)
        instanceXML.attributes["CLASSNAME"] == "MSFT_nxAvailableUpdatesResource"
    end

    def instanceXMLtoHash(instanceXML)
        ret = {}
        propertyXPath = "PROPERTY"
        instanceXML.elements.each(propertyXPath) { |inst| #AnonFunc in rb.
            name = inst.attributes['NAME']
            rexmlText = REXML::XPath.first(inst, 'VALUE').get_text # TODO escape unicode chars like "&amp;"
            value = rexmlText ? rexmlText.value.strip : ''
            ret[name] = value
        }
        ret
    end

    def availableUpdatesXMLtoHash(availableUpdatesXML, os_short_name)
        availableUpdatesHash = instanceXMLtoHash(availableUpdatesXML)
        ret = {}
        version = availableUpdatesHash["Version"].count(':') > 0 ? availableUpdatesHash["Version"] : "0:" + availableUpdatesHash["Version"]
        ret["CollectionName"] = availableUpdatesHash["Name"] + @@delimiter + version + @@delimiter + os_short_name
        ret["PackageName"] = availableUpdatesHash["Name"]
        ret["Architecture"] = availableUpdatesHash.key?("Architecture") ? availableUpdatesHash["Architecture"] : nil
        ret["PackageVersion"] = version
        ret["Repository"] = availableUpdatesHash.key?("Repository") ? availableUpdatesHash["Repository"] : nil
        ret["PackageClassification"] = availableUpdatesHash.key?("Classification") ? availableUpdatesHash["Classification"] : nil
        ret["Installed"] = false
        ret["UpdateState"] = "Needed"
        if (Integer(availableUpdatesHash["BuildDate"]) rescue false)
            ret["Timestamp"] = OMS::Common.format_time(availableUpdatesHash["BuildDate"].to_i)
        end
        ret
    end

    def availableHeartbeatItem()        
        ret = {}
        ret["CollectionName"] = "HeartbeatData" + @@delimiter + 
                                "0.0.UpdateManagement.0"+ @@delimiter + "Heartbeat"
        ret["PackageName"] = "UpdateManagementHeartbeat"
        ret["Architecture"] = "all"
        ret["PackageVersion"] = nil
        ret["Repository"] = nil
        ret["PackageClassification"] = nil
        ret["Installed"] = false
        ret["UpdateState"] = "NotNeeded"        
        ret
    end

    def installedPackageXMLtoHash(packageXML, os_short_name)
        packageHash = instanceXMLtoHash(packageXML)
        ret = {}
        version = packageHash["Version"].count(':') > 0 ? packageHash["Version"] : "0:" + packageHash["Version"]
        ret["CollectionName"] = packageHash["Name"] + @@delimiter + version + @@delimiter + os_short_name
        ret["PackageName"] = packageHash["Name"]
        ret["Architecture"] = packageHash.key?("Architecture") ? packageHash["Architecture"] : nil
        ret["PackageVersion"] = version  
        ret["Size"] = packageHash["Size"]
        ret["Repository"] = packageHash.key?("Repository") ? packageHash["Repository"] : nil
        ret["PackageClassification"] = packageHash.key?("Classification") ? packageHash["Classification"] : nil
        ret["Installed"] = true
        ret["UpdateState"] = "NotNeeded"
        if (Integer(packageHash["InstalledOn"]) rescue false)
            ret["Timestamp"] = OMS::Common.format_time(packageHash["InstalledOn"].to_i)
        end
        ret
    end

    def strToXML(xml_string)
        xml_unescaped_string = CGI::unescapeHTML(xml_string)
        REXML::Document.new xml_unescaped_string
    end

    # Returns an array of xml instances (all types)
    def getInstancesXML(inventoryXML)
        instances = []
        xpathFilter = "INSTANCE/PROPERTY.ARRAY/VALUE.ARRAY/VALUE/INSTANCE"
        inventoryXML.elements.each(xpathFilter) { |inst| instances << inst }
        instances
    end

    def removeDuplicateCollectionNames(data_items)
        collection_names = Set.new
        data_items.select { |data_item|
            collection_names.add?(data_item["CollectionName"]) && true || false
        }
    end

    def transform_and_wrap(
        inventoryXMLstr, 
        host, 
        time, 
        force_send_run_interval = 86400,
        osNameParam = nil, 
        osFullNameParam = nil,
        osVersionParam = nil, 
        osShortNameParam = nil)

        agentId = getAgentId()
        hostOSDetailsMap = getHostOSDetails()
       
        # Taking the parameters - Helps in mocking, in the case of tests.
        osName =  (osNameParam == nil) ? hostOSDetailsMap["OSName"] : osNameParam
        osVersion = (osVersionParam == nil) ? hostOSDetailsMap["OSVersion"] : osVersionParam
        osFullName = (osFullNameParam == nil) ? hostOSDetailsMap["OSFullName"] : osFullNameParam
        osShortName = getOSShortName(osShortNameParam, osVersion)

        # Do not send duplicate data if we are not forced to
        hash = Digest::SHA256.hexdigest(inventoryXMLstr)
        OMS::Log.info_once("LinuxUpdates : Sending available updates information data. Hash=#{hash[0..5]}")

        # Extract the instances in xml format
        inventoryXML = strToXML(inventoryXMLstr)
        instancesXML = getInstancesXML(inventoryXML)

        # Split installedPackages from services 
        installedPackagesXML = instancesXML.select { |instanceXML| isInstalledPackageInstanceXML(instanceXML) }
        availableUpdatesXML = instancesXML.select { |instanceXML| isAvailableUpdateInstanceXML(instanceXML) }

        # Convert to xml to hash/json representation
        installedPackages = installedPackagesXML.map! { |installedPackage|  installedPackageXMLtoHash(installedPackage, osShortName)}
        availableUpdates = availableUpdatesXML.map! { |availableUpdate|  availableUpdatesXMLtoHash(availableUpdate, osShortName)}

        # Remove duplicate services because duplicate CollectionNames are not supported. TODO implement ordinal solution
        installedPackages = removeDuplicateCollectionNames(installedPackages)
        availableUpdates = removeDuplicateCollectionNames(availableUpdates)
        #Today the agent doesn't send any data if the machine is not missing any available updates. 
        #This leads to uncertainty whether the machine is really up to date or it is not sending data eventhough updates are missing. 
        #with this change,the agent will send heartbeat item whether the machine is missing updates or not.     
        heartbeatItem = availableHeartbeatItem()
        collections = [heartbeatItem]
        
        if (installedPackages.size > 0)
            collections += installedPackages
        end
        
        if  (availableUpdates.size > 0)
            collections += availableUpdates
        end

        timestamp = OMS::Common.format_time(time)
        wrapper = {
            "DataType"=>"LINUX_UPDATES_SNAPSHOT_BLOB",
            "IPName"=>"Updates",
            "DataItems"=>[{
                    "Timestamp" => timestamp,
                    "Host" => host,
                    "AgentId" => agentId,
                    "OSType" => "Linux",
                    "OSName" => osName,
                    "OSVersion" => osVersion,
                    "OSFullName" => osFullName,
                    "Collections"=> collections
                }]}
          OMS::Log.info_once("LinuxUpdates : installedPackages x #{installedPackages.size}, 
                                        availableUpdates x #{availableUpdates.size}")
          return wrapper
    end
	
end
