#!powershell

# Copyright: (c) 2018, 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
#Requires -Module Ansible.ModuleUtils.Backup

Set-StrictMode -Version 2

function Copy-Xml($dest, $src, $xmlorig) {
    if ($src.get_NodeType() -eq "Text") {
        $dest.set_InnerText($src.get_InnerText())
    }

    if ($src.get_HasAttributes()) {
        foreach ($attr in $src.get_Attributes()) {
            $dest.SetAttribute($attr.get_Name(), $attr.get_Value())
        }
    }

    if ($src.get_HasChildNodes()) {
        foreach ($childnode in $src.get_ChildNodes()) {
            if ($childnode.get_NodeType() -eq "Element") {
                $newnode = $xmlorig.CreateElement($childnode.get_Name(), $xmlorig.get_DocumentElement().get_NamespaceURI())
                Copy-Xml -dest $newnode -src $childnode -xmlorig $xmlorig
                $dest.AppendChild($newnode) | Out-Null
            }
            elseif ($childnode.get_NodeType() -eq "Text") {
                $dest.set_InnerText($childnode.get_InnerText())
            }
        }
    }
}

function Compare-XmlDoc($actual, $expected) {
    if ($actual.get_Name() -ne $expected.get_Name()) {
        throw "Actual name not same as expected: actual=" + $actual.get_Name() + ", expected=" + $expected.get_Name()
    }
    ##attributes...

    if (($actual.get_NodeType() -eq "Element") -and ($expected.get_NodeType() -eq "Element")) {
        if ($actual.get_HasAttributes() -and $expected.get_HasAttributes()) {
            if ($actual.get_Attributes().Count -ne $expected.get_Attributes().Count) {
                throw "attribute mismatch for actual=" + $actual.get_Name()
            }
            for ($i = 0; $i -lt $expected.get_Attributes().Count; $i = $i + 1) {
                if ($expected.get_Attributes()[$i].get_Name() -ne $actual.get_Attributes()[$i].get_Name()) {
                    throw "attribute name mismatch for actual=" + $actual.get_Name()
                }
                if ($expected.get_Attributes()[$i].get_Value() -ne $actual.get_Attributes()[$i].get_Value()) {
                    throw "attribute value mismatch for actual=" + $actual.get_Name()
                }
            }
        }

        if (($actual.get_HasAttributes() -and !$expected.get_HasAttributes()) -or (!$actual.get_HasAttributes() -and $expected.get_HasAttributes())) {
            throw "attribute presence mismatch for actual=" + $actual.get_Name()
        }
    }

    ##children
    if ($expected.get_ChildNodes().Count -ne $actual.get_ChildNodes().Count) {
        throw "child node mismatch. for actual=" + $actual.get_Name()
    }

    for ($i = 0; $i -lt $expected.get_ChildNodes().Count; $i = $i + 1) {
        if (-not $actual.get_ChildNodes()[$i]) {
            throw "actual missing child nodes. for actual=" + $actual.get_Name()
        }
        Compare-XmlDoc $expected.get_ChildNodes()[$i] $actual.get_ChildNodes()[$i]
    }

    if ($expected.get_InnerText()) {
        if ($expected.get_InnerText() -ne $actual.get_InnerText()) {
            throw "inner text mismatch for actual=" + $actual.get_Name()
        }
    }
    elseif ($actual.get_InnerText()) {
        throw "actual has inner text but expected does not for actual=" + $actual.get_Name()
    }
}


function Save-ChangedXml($xmlorig, $result, $message, $check_mode, $backup) {
    $result.changed = $true
    if (-Not $check_mode) {
        if ($backup) {
            $result.backup_file = Backup-File -path $dest -WhatIf:$check_mode
            # Ensure backward compatibility (deprecate in future)
            $result.backup = $result.backup_file
        }
        $xmlorig.Save($dest)
        $result.msg = $message
    }
    else {
        $result.msg += " check mode"
    }
}

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

$debug_level = Get-AnsibleParam -obj $params -name "_ansible_verbosity" -type "int"
$debug = $debug_level -gt 2

$dest = Get-AnsibleParam $params "path" -type "path" -FailIfEmpty $true -aliases "dest", "file"
$fragment = Get-AnsibleParam $params "fragment" -type "str" -aliases "xmlstring"
$xpath = Get-AnsibleParam $params "xpath" -type "str" -FailIfEmpty $true
$backup = Get-AnsibleParam $params "backup" -type "bool" -Default $false
$type = Get-AnsibleParam $params "type" -type "str" -Default "element" -ValidateSet "element", "attribute", "text"
$attribute = Get-AnsibleParam $params "attribute" -type "str" -FailIfEmpty ($type -eq "attribute")
$state = Get-AnsibleParam $params "state" -type "str" -Default "present"
$count = Get-AnsibleParam $params "count" -type "bool" -Default $false

$result = @{
    changed = $false
}

If (-Not (Test-Path -LiteralPath $dest -PathType Leaf)) {
    Fail-Json $result "Specified path $dest does not exist or is not a file."
}

$xmlorig = New-Object -TypeName System.Xml.XmlDocument
$xmlorig.XmlResolver = $null
Try {
    $xmlorig.Load($dest)
}
Catch {
    Fail-Json $result "Failed to parse file at '$dest' as an XML document: $($_.Exception.Message)"
}

$namespaceMgr = New-Object System.Xml.XmlNamespaceManager $xmlorig.NameTable
$namespace = $xmlorig.DocumentElement.NamespaceURI
$localname = $xmlorig.DocumentElement.LocalName

$namespaceMgr.AddNamespace($xmlorig.$localname.SchemaInfo.Prefix, $namespace)

$nodeList = $xmlorig.SelectNodes($xpath, $namespaceMgr)
$nodeListCount = $nodeList.get_Count()
if ($count) {
    $result.count = $nodeListCount
    if (-not $fragment) {
        Exit-Json $result
    }
}
## Exit early if xpath did not match any nodes
if ($nodeListCount -eq 0) {
    $result.msg = "The supplied xpath did not match any nodes.  If this is unexpected, check your xpath is valid for the xml file at supplied path."
    Exit-Json $result
}

$changed = $false
$result.msg = "not changed"

if ($type -eq "element") {
    if ($state -eq "absent") {

        $removals = [System.Collections.Generic.List[String]]@()

        foreach ($node in $nodeList) {
            # there are some nodes that match xpath, delete without comparing them to fragment
            if (-Not $check_mode) {
                [void]$node.get_ParentNode().RemoveChild($node)
                $changed = $true
            }

            if ($debug) {
                $removals.Add($node.get_OuterXml())
            }
        }

        if ($removals) {
            $result.removed = $removals -join ", "
        }
    }
    else {
        # state -eq 'present'
        $xmlfragment = $null
        Try {
            $xmlfragment = [xml]$fragment
        }
        Catch {
            Fail-Json $result "Failed to parse fragment as XML: $($_.Exception.Message)"
        }

        foreach ($node in $nodeList) {
            $candidate = $xmlorig.CreateElement($xmlfragment.get_DocumentElement().get_Name(), $xmlorig.get_DocumentElement().get_NamespaceURI())
            Copy-Xml -dest $candidate -src $xmlfragment.DocumentElement -xmlorig $xmlorig

            if ($node.get_NodeType() -eq "Document") {
                $node = $node.get_DocumentElement()
            }

            if ($node.ChildNodes.Count -eq 0) {
                $elements = @($node)
            }
            else {
                $elements = $node.get_ChildNodes()
            }

            [bool]$present = $false
            [bool]$changed = $false
            if ($elements.get_Count()) {
                if ($debug) {
                    $err = @()
                    $result.err = { $err }.Invoke()
                }
                foreach ($element in $elements) {
                    try {
                        Compare-XmlDoc $candidate $element
                        $present = $true
                        break
                    }
                    catch {
                        if ($debug) {
                            $result.err.Add($_.Exception.ToString())
                        }
                    }
                }
                if (-Not $present -and ($state -eq "present")) {
                    [void]$node.AppendChild($candidate)
                    $result.msg = $result.msg + "xml added "
                    $changed = $true
                }
            }
        }
    }
}
elseif ($type -eq "text") {
    foreach ($node in $nodeList) {
        if ($node.get_InnerText() -ne $fragment) {
            $node.set_InnerText($fragment)
            $changed = $true
        }
    }
}
elseif ($type -eq "attribute") {
    foreach ($node in $nodeList) {
        if ($state -eq 'present') {
            if ($node.NodeType -eq 'Attribute') {
                if ($node.Name -eq $attribute -and $node.Value -ne $fragment ) {
                    # this is already the attribute with the right name, so just set its value to match fragment
                    $node.Value = $fragment
                    $changed = $true
                }
            }
            else {
                # assume NodeType is Element
                if (!$node.HasAttribute($attribute) -or ($node.$attribute -ne $fragment)) {
                    if (!$node.HasAttribute($attribute)) {
                        # add attribute to Element if missing
                        $node.SetAttributeNode($attribute, $xmlorig.get_DocumentElement().get_NamespaceURI())
                    }
                    #set the attribute into the element
                    $node.SetAttribute($attribute, $fragment)
                    $changed = $true
                }
            }
        }
        elseif ($state -eq 'absent') {
            if ($node.NodeType -eq 'Attribute') {
                $attrNode = [System.Xml.XmlAttribute]$node
                $parent = $attrNode.OwnerElement
                $parent.RemoveAttribute($attribute)
                $changed = $true
            }
            else {
                # element node processing
                if ($node.Name -eq $attribute ) {
                    # note not caring about the state of 'fragment' at this point
                    $node.RemoveAttribute($attribute)
                    $changed = $true
                }
            }
        }
        else {
            Add-Warning $result "Unexpected state when processing attribute $($attribute), add was $add, state was $state"
        }
    }
}
if ($changed) {
    if ($state -eq "absent") {
        $summary = "$type removed"
    }
    else {
        $summary = "$type changed"
    }
    Save-ChangedXml -xmlorig $xmlorig -result $result -message $summary -check_mode $check_mode -backup $backup
}

Exit-Json $result
