#!powershell

# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

#Requires -Module Ansible.ModuleUtils.Legacy

$ErrorActionPreference = "Stop"

$params = Parse-Args $args -supports_check_mode $true
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false

$hotfix_kb = Get-AnsibleParam -obj $params -name "hotfix_kb" -type "str"
$hotfix_identifier = Get-AnsibleParam -obj $params -name "hotfix_identifier" -type "str"
$state = Get-AnsibleParam -obj $params -name "state" -type "state" -default "present" -validateset "absent", "present"
$source = Get-AnsibleParam -obj $params -name "source" -type "path"

$result = @{
    changed = $false
    reboot_required = $false
}

if (Get-Module -Name DISM -ListAvailable) {
    Import-Module -Name DISM
}
else {
    # Server 2008 R2 doesn't have the DISM module installed on the path, check the Windows ADK path
    $adk_root = [System.Environment]::ExpandEnvironmentVariables("%PROGRAMFILES(X86)%\Windows Kits\*\Assessment and Deployment Kit\Deployment Tools\amd64\DISM")
    if (Test-Path -LiteralPath $adk_root) {
        Import-Module -Name (Get-Item -LiteralPath $adk_root).FullName
    }
    else {
        Fail-Json $result "The DISM PS module needs to be installed, this can be done through the windows-adk chocolately package"
    }
}


Function Expand-MSU($msu) {
    $temp_path = [IO.Path]::GetTempPath()
    $temp_foldername = [Guid]::NewGuid()
    $output_path = Join-Path -Path $temp_path -ChildPath $temp_foldername
    New-Item -Path $output_path -ItemType Directory | Out-Null

    $expand_args = @($msu, $output_path, "-F:*")

    try {
        &expand.exe $expand_args | Out-NUll
    }
    catch {
        Fail-Json $result "failed to run expand.exe $($expand_args): $($_.Exception.Message)"
    }
    if ($LASTEXITCODE -ne 0) {
        Fail-Json $result "failed to run expand.exe $($expand_args): RC = $LASTEXITCODE"
    }

    return $output_path
}

Function Get-HotfixMetadataFromName($name) {
    try {
        $dism_package_info = Get-WindowsPackage -Online -PackageName $name
    }
    catch {
        # build a basic stub for a missing result
        $dism_package_info = @{
            PackageState = "NotPresent"
            Description = ""
            PackageName = $name
        }
    }

    if ($dism_package_info.Description -match "(KB\d*)") {
        $hotfix_kb = $Matches[0]
    }
    else {
        $hotfix_kb = "UNKNOWN"
    }

    $metadata = @{
        name = $dism_package_info.PackageName
        state = $dism_package_info.PackageState
        kb = $hotfix_kb
    }

    return $metadata
}

Function Get-HotfixMetadataFromFile($extract_path) {
    # MSU contents https://support.microsoft.com/en-us/help/934307/description-of-the-windows-update-standalone-installer-in-windows
    $metadata_path = Get-ChildItem -LiteralPath $extract_path | Where-Object { $_.Extension -eq ".xml" }
    if ($null -eq $metadata_path) {
        Fail-Json $result "failed to get metadata xml inside MSU file, cannot get hotfix metadata required for this task"
    }
    [xml]$xml = Get-Content -LiteralPath $metadata_path.FullName

    $xml.unattend.servicing.package.source.location | ForEach-Object {
        $cab_source_filename = Split-Path -Path $_ -Leaf
        $cab_file = Join-Path -Path $extract_path -ChildPath $cab_source_filename

        try {
            $dism_package_info = Get-WindowsPackage -Online -PackagePath $cab_file
        }
        catch {
            Fail-Json $result "failed to get DISM package metadata from path $($extract_path): $($_.Exception.Message)"
        }
        if ($dism_package_info.Applicable -eq $false) {
            Fail-Json $result "hotfix package is not applicable for this server"
        }

        $package_properties_path = Get-ChildItem -LiteralPath $extract_path | Where-Object { $_.Extension -eq ".txt" }
        if ($null -eq $package_properties_path) {
            $hotfix_kb = "UNKNOWN"
        }
        else {
            $package_ini = Get-Content -LiteralPath $package_properties_path.FullName
            $entry = $package_ini | Where-Object { $_.StartsWith("KB Article Number") }
            if ($null -eq $entry) {
                $hotfix_kb = "UNKNOWN"
            }
            else {
                $hotfix_kb = ($entry -split '=')[-1]
                $hotfix_kb = "KB$($hotfix_kb.Substring(1, $hotfix_kb.Length - 2))"
            }
        }

        [pscustomobject]@{
            path = $cab_file
            name = $dism_package_info.PackageName
            state = $dism_package_info.PackageState
            kb = $hotfix_kb
        }
    }
}

Function Get-HotfixMetadataFromKB($kb) {
    # I really hate doing it this way
    $packages = Get-WindowsPackage -Online
    $identifier = $packages | Where-Object { $_.PackageName -like "*$kb*" }

    if ($null -eq $identifier) {
        # still haven't found the KB, need to loop through the results and check the description
        foreach ($package in $packages) {
            $raw_metadata = Get-HotfixMetadataFromName -name $package.PackageName
            if ($raw_metadata.kb -eq $kb) {
                $metadata = $raw_metadata
                break
            }
        }

        # if we still haven't found the package then we need to throw an error
        if ($null -eq $metadata) {
            Fail-Json $result "failed to get DISM package from KB, to continue specify hotfix_identifier instead"
        }
    }
    else {
        $metadata = Get-HotfixMetadataFromName -name $identifier.PackageName
    }

    return $metadata
}

if ($state -eq "absent") {
    # uninstall hotfix
    # this is a pretty poor way of doing this, is there a better way?

    if ($null -ne $hotfix_identifier) {
        $hotfix_metadata = Get-HotfixMetadataFromName -name $hotfix_identifier
    }
    elseif ($null -ne $hotfix_kb) {
        $hotfix_install_info = Get-Hotfix -Id $hotfix_kb -ErrorAction SilentlyContinue
        if ($null -ne $hotfix_install_info) {
            $hotfix_metadata = Get-HotfixMetadataFromKB -kb $hotfix_kb
        }
        else {
            $hotfix_metadata = @{state = "NotPresent" }
        }
    }
    else {
        Fail-Json $result "either hotfix_identifier or hotfix_kb needs to be set when state=absent"
    }

    # how do we want to deal with the other states?
    if ($hotfix_metadata.state -eq "UninstallPending") {
        $result.identifier = $hotfix_metadata.name
        $result.kb = $hotfix_metadata.kb
        $result.reboot_required = $true
    }
    elseif ($hotfix_metadata.state -eq "Installed") {
        $result.identifier = $hotfix_metadata.name
        $result.kb = $hotfix_metadata.kb

        if (-not $check_mode) {
            try {
                $remove_result = Remove-WindowsPackage -Online -PackageName $hotfix_metadata.name -NoRestart
            }
            catch {
                Fail-Json $result "failed to remove package $($hotfix_metadata.name): $($_.Exception.Message)"
            }
            $result.reboot_required = $remove_Result.RestartNeeded
        }

        $result.changed = $true
    }
}
else {
    if ($null -eq $source) {
        Fail-Json $result "source must be set when state=present"
    }
    if (-not (Test-Path -LiteralPath $source -PathType Leaf)) {
        Fail-Json $result "the path set for source $source does not exist or is not a file"
    }

    # while we do extract the file in check mode we need to do so for valid checking
    $extract_path = Expand-MSU -msu $source
    try {
        $hotfix_metadata = Get-HotfixMetadataFromFile -extract_path $extract_path

        # validate the hotfix matches if the hotfix id has been passed in
        if ($null -ne $hotfix_identifier) {
            if ($hotfix_metadata.name -ne $hotfix_identifier) {
                $msg = -join @(
                    "the hotfix identifier $hotfix_identifier does not match with the source msu identifier $($hotfix_metadata.name), "
                    "please omit or specify the correct identifier to continue"
                )
                Fail-Json $result $msg
            }
        }
        if ($null -ne $hotfix_kb) {
            if ($hotfix_metadata.kb -ne $hotfix_kb) {
                $msg = -join @(
                    "the hotfix KB $hotfix_kb does not match with the source msu KB $($hotfix_metadata.kb), "
                    "please omit or specify the correct KB to continue"
                )
                Fail-Json $result $msg
            }
        }

        $result.identifiers = @($hotfix_metadata.name)
        $result.identifier = $result.identifiers[0]
        $result.kbs = @($hotfix_metadata.kb)
        $result.kb = $result.kbs[0]

        # how do we want to deal with other states
        $any_pending = $false
        $any_not_installed = $false
        foreach ($package in $hotfix_metadata) {
            if ($package.state -eq "InstallPending") {
                $any_pending = $true
            }
            elseif ($package.state -ne "Installed") {
                $any_not_installed = $true
            }
        }

        # If DISM reports everything as installed, double check with Get-Hotfix if we have a KB identifier.
        # This addresses cases where DISM falsely reports 'Installed' for meta-packages (e.g. Multiple_Packages~~~~0.0.0.0).
        if (-not $any_not_installed -and $result.kb -ne "UNKNOWN") {
            if (-not (Get-Hotfix -Id $result.kb -ErrorAction SilentlyContinue)) {
                $any_not_installed = $true
            }
        }

        if ($any_not_installed) {
            if (-not $check_mode) {
                try {
                    $install_result = @(
                        Foreach ($path in $hotfix_metadata.path) {
                            Add-WindowsPackage -Online -PackagePath $path -NoRestart
                        }
                    )
                }
                catch {
                    Fail-Json $result "failed to add windows package from path $($hotfix_metadata.path): $($_.Exception.Message)"
                }
                $result.reboot_required = [bool]($install_result.RestartNeeded -contains $true)
            }
            $result.changed = $true
        }
        elseif ($any_pending) {
            $result.reboot_required = $true
        }
    }
    finally {
        Remove-Item -LiteralPath $extract_path -Force -Recurse
    }
}

Exit-Json $result
