#!powershell

# Copyright: (c) 2015, Paul Durivage <paul.durivage@rackspace.com>
# Copyright: (c) 2015, Tal Auslander <tal@cloudshare.com>
# Copyright: (c) 2017, Dag Wieers <dag@wieers.com>
# Copyright: (c) 2019, Viktor Utkin <viktor_utkin@epam.com>
# Copyright: (c) 2019, Uladzimir Klybik <uladzimir_klybik@epam.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

#AnsibleRequires -CSharpUtil Ansible.Basic
#Requires -Module Ansible.ModuleUtils.FileUtil
#AnsibleRequires -PowerShell ..module_utils.WebRequest

$spec = @{
    options = @{
        url = @{ type="str"; required=$true }
        dest = @{ type='path'; required=$true }
        force = @{ type='bool'; default=$true }
        checksum = @{ type='str' }
        checksum_algorithm = @{ type='str'; default='sha1'; choices = @("md5", "sha1", "sha256", "sha384", "sha512") }
        checksum_url = @{ type='str' }

        # Defined for ease of use and backwards compatibility
        url_method = @{
            aliases = "method"
        }
        url_timeout = @{
            aliases = "timeout"
        }

        # Defined for the alias backwards compatibility, remove once aliases are removed
        url_username = @{
            aliases = @("user", "username")
            deprecated_aliases = @(
                @{ name = "user"; date = [DateTime]::ParseExact("2022-07-01", "yyyy-MM-dd", $null); collection_name = 'ansible.windows' },
                @{ name = "username"; date = [DateTime]::ParseExact("2022-07-01", "yyyy-MM-dd", $null); collection_name = 'ansible.windows' }
            )
        }
        url_password = @{
            aliases = @("password")
            deprecated_aliases = @(
                @{ name = "password"; date = [DateTime]::ParseExact("2022-07-01", "yyyy-MM-dd", $null); collection_name = 'ansible.windows' }
            )
        }
    }
    mutually_exclusive = @(
        ,@('checksum', 'checksum_url')
    )
    supports_check_mode = $true
}
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec, @(Get-AnsibleWindowsWebRequestSpec))

$url = $module.Params.url
$dest = $module.Params.dest
$force = $module.Params.force
$checksum = $module.Params.checksum
$checksum_algorithm = $module.Params.checksum_algorithm
$checksum_url = $module.Params.checksum_url

$module.Result.elapsed = 0
$module.Result.url = $url

Function Get-ChecksumFromUri {
    param(
        [Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
        [Parameter(Mandatory=$true)][Uri]$Uri,
        [Uri]$SourceUri
    )

    $script = {
        param($Response, $Stream)

        $read_stream = New-Object -TypeName System.IO.StreamReader -ArgumentList $Stream
        $web_checksum = $read_stream.ReadToEnd()
        $basename = (Split-Path -Path $SourceUri.LocalPath -Leaf)
        $basename = [regex]::Escape($basename)
        $web_checksum_str = $web_checksum -split '\r?\n' | Select-String -Pattern $("\s+\.?\/?\\?" + $basename + "\s*$")
        if (-not $web_checksum_str) {
            $Module.FailJson("Checksum record not found for file name '$basename' in file from url: '$Uri'")
        }

        $web_checksum_str_splitted = $web_checksum_str[0].ToString().split(" ", 2)
        $hash_from_file = $web_checksum_str_splitted[0].Trim()
        # Remove any non-alphanumeric characters
        $hash_from_file = $hash_from_file -replace '\W+', ''

        Write-Output -InputObject $hash_from_file
    }
    $web_request = Get-AnsibleWindowsWebRequest -Uri $Uri -Module $Module

    try {
        Invoke-AnsibleWindowsWebRequest -Module $Module -Request $web_request -Script $script
    } catch {
        $Module.FailJson("Error when getting the remote checksum from '$Uri'. $($_.Exception.Message)", $_)
    }
}

Function Compare-ModifiedFile {
    <#
    .SYNOPSIS
    Compares the remote URI resource against the local Dest resource. Will
    return true if the LastWriteTime/LastModificationDate of the remote is
    newer than the local resource date.
    #>
    param(
        [Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
        [Parameter(Mandatory=$true)][Uri]$Uri,
        [Parameter(Mandatory=$true)][String]$Dest
    )

    $dest_last_mod = (Get-AnsibleItem -Path $Dest).LastWriteTimeUtc

    # If the URI is a file we don't need to go through the whole WebRequest
    if ($Uri.IsFile) {
        $src_last_mod = (Get-AnsibleItem -Path $Uri.AbsolutePath).LastWriteTimeUtc
    } else {
        $web_request = Get-AnsibleWindowsWebRequest -Uri $Uri -Module $Module
        $web_request.Method = switch ($web_request.GetType().Name) {
            FtpWebRequest { [System.Net.WebRequestMethods+Ftp]::GetDateTimestamp }
            HttpWebRequest { [System.Net.WebRequestMethods+Http]::Head }
        }
        $script = { param($Response, $Stream); $Response.LastModified }

        try {
            $src_last_mod = Invoke-AnsibleWindowsWebRequest -Module $Module -Request $web_request -Script $script
        } catch {
            $Module.FailJson("Error when requesting 'Last-Modified' date from '$Uri'. $($_.Exception.Message)", $_)
        }
    }

    # Return $true if the Uri LastModification date is newer than the Dest LastModification date
    ((Get-Date -Date $src_last_mod).ToUniversalTime() -gt $dest_last_mod)
}

Function Get-Checksum {
    param(
        [Parameter(Mandatory=$true)][String]$Path,
        [String]$Algorithm = "sha1"
    )

    switch ($Algorithm) {
        'md5' { $sp = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
        'sha1' { $sp = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
        'sha256' { $sp = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
        'sha384' { $sp = New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
        'sha512' { $sp = New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
    }

    $fs = [System.IO.File]::Open($Path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read,
        [System.IO.FileShare]::ReadWrite)
    try {
        $hash = [System.BitConverter]::ToString($sp.ComputeHash($fs)).Replace("-", "").ToLower()
    } finally {
        $fs.Dispose()
    }
    return $hash
}

Function Invoke-DownloadFile {
    param(
        [Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
        [Parameter(Mandatory=$true)][Uri]$Uri,
        [Parameter(Mandatory=$true)][String]$Dest,
        [String]$Checksum,
        [String]$ChecksumAlgorithm
    )

    # Check $dest parent folder exists before attempting download, which avoids unhelpful generic error message.
    $dest_parent = Split-Path -LiteralPath $Dest
    if (-not (Test-Path -LiteralPath $dest_parent -PathType Container)) {
        $module.FailJson("The path '$dest_parent' does not exist for destination '$Dest', or is not visible to the current user.  Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs.")
    }

    $download_script = {
        param($Response, $Stream)

        # Download the file to a temporary directory so we can compare it
        $tmp_dest = Join-Path -Path $Module.Tmpdir -ChildPath ([System.IO.Path]::GetRandomFileName())
        $fs = [System.IO.File]::Create($tmp_dest)
        try {
            $Stream.CopyTo($fs)
            $fs.Flush()
        } finally {
            $fs.Dispose()
        }
        $tmp_checksum = Get-Checksum -Path $tmp_dest -Algorithm $ChecksumAlgorithm
        $Module.Result.checksum_src = $tmp_checksum

        # If the checksum has been set, verify the checksum of the remote against the input checksum.
        if ($Checksum -and $Checksum -ne $tmp_checksum) {
            $Module.FailJson(("The checksum for {0} did not match '{1}', it was '{2}'" -f $Uri, $Checksum, $tmp_checksum))
        }

        $download = $true
        if (Test-Path -LiteralPath $Dest) {
            # Validate the remote checksum against the existing downloaded file
            $dest_checksum = Get-Checksum -Path $Dest -Algorithm $ChecksumAlgorithm

            # If we don't need to download anything, save the dest checksum so we don't waste time calculating it
            # again at the end of the script
            if ($dest_checksum -eq $tmp_checksum) {
                $download = $false
                $Module.Result.checksum_dest = $dest_checksum
                $Module.Result.size = (Get-AnsibleItem -Path $Dest).Length
            }
        }

        if ($download) {
            Copy-Item -LiteralPath $tmp_dest -Destination $Dest -Force -WhatIf:$Module.CheckMode > $null
            $Module.Result.changed = $true
        }
    }
    $web_request = Get-AnsibleWindowsWebRequest -Uri $Uri -Module $Module

    try {
        Invoke-AnsibleWindowsWebRequest -Module $Module -Request $web_request -Script $download_script
    } catch {
        $Module.FailJson("Error downloading '$Uri' to '$Dest': $($_.Exception.Message)", $_)
    }
}

# Use last part of url for dest file name if a directory is supplied for $dest
if (Test-Path -LiteralPath $dest -PathType Container) {
    $uri = [System.Uri]$url
    $basename = Split-Path -Path $uri.LocalPath -Leaf
    if ($uri.LocalPath -and $uri.LocalPath -ne '/' -and $basename) {
        $url_basename = Split-Path -Path $uri.LocalPath -Leaf
        $dest = Join-Path -Path $dest -ChildPath $url_basename
    } else {
        $dest = Join-Path -Path $dest -ChildPath $uri.Host
    }

    # Ensure we have a string instead of a PS object to avoid serialization issues
    $dest = $dest.ToString()
} elseif (([System.IO.Path]::GetFileName($dest)) -eq '') {
    # We have a trailing path separator
    $module.FailJson("The destination path '$dest' does not exist, or is not visible to the current user.  Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs.")
}

$module.Result.dest = $dest

if ($checksum) {
    $checksum = $checksum.Trim().ToLower()
}
if ($checksum_algorithm) {
    $checksum_algorithm = $checksum_algorithm.Trim().ToLower()
}
if ($checksum_url) {
    $checksum_url = $checksum_url.Trim()
}

# Check for case $checksum variable contain url. If yes, get file data from url and replace original value in $checksum
if ($checksum_url) {
    $checksum_uri = [System.Uri]$checksum_url
    if ($checksum_uri.Scheme -notin @("file", "ftp", "http", "https")) {
        $module.FailJson("Unsupported 'checksum_url' value for '$dest': '$checksum_url'")
    }

    $checksum = Get-ChecksumFromUri -Module $Module -Uri $checksum_uri -SourceUri $url
}

if ($force -or -not (Test-Path -LiteralPath $dest)) {
    # force=yes or dest does not exist, download the file
    # Note: Invoke-DownloadFile will compare the checksums internally if dest exists
    Invoke-DownloadFile -Module $module -Uri $url -Dest $dest -Checksum $checksum `
        -ChecksumAlgorithm $checksum_algorithm
} else {
    # force=no, we want to check the last modified dates and only download if they don't match
    $is_modified = Compare-ModifiedFile -Module $module -Uri $url -Dest $dest
    if ($is_modified) {
        Invoke-DownloadFile -Module $module -Uri $url -Dest $dest -Checksum $checksum `
            -ChecksumAlgorithm $checksum_algorithm
   }
}

if ((-not $module.Result.ContainsKey("checksum_dest")) -and (Test-Path -LiteralPath $dest)) {
    # Calculate the dest file checksum if it hasn't already been done
    $module.Result.checksum_dest = Get-Checksum -Path $dest -Algorithm $checksum_algorithm
    $module.Result.size = (Get-AnsibleItem -Path $dest).Length
}

$module.ExitJson()
