LibreDevOpsHelpers.AzureFunctionApps/LibreDevOpsHelpers.AzureFunctionApps.psm1

function Compress-FunctionAppSource
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path $_ -PathType Container })]
        [string]$SourcePath,

        [Parameter(Mandatory)]
        [string]$ZipPath,

        [switch]$Overwrite
    )

    $inv = $MyInvocation.MyCommand.Name
    try
    {
        if (Test-Path $ZipPath)
        {
            if ($Overwrite)
            {
                _LogMessage -Level INFO -Message "Overwriting existing ZIP: $ZipPath" -InvocationName $inv
                Remove-Item -Path $ZipPath -Force
            }
            else
            {
                throw "ZipPath already exists: $ZipPath (use -Overwrite)."
            }
        }

        _LogMessage -Level INFO -Message "Creating deployment package → $ZipPath" -InvocationName $inv

        Add-Type -AssemblyName 'System.IO.Compression.FileSystem'
        [System.IO.Compression.ZipFile]::CreateFromDirectory($SourcePath, $ZipPath)

        if (-not (Test-Path $ZipPath))
        {
            throw 'ZIP creation failed.'
        }

        $sizeMb = [math]::Round((Get-Item $ZipPath).Length / 1MB, 2)
        _LogMessage -Level INFO -Message "ZIP package size: $sizeMb MB" -InvocationName $inv
    }
    catch
    {
        _LogMessage -Level ERROR -Message $_.Exception.Message -InvocationName $inv
        throw
    }
}


function Invoke-FunctionAppDeployZip
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ResourceGroup,
        [Parameter(Mandatory)][string]$FunctionAppName,
        [Parameter(Mandatory)][string]$ZipPath,
        [switch]$RestartOnFinish,
        [string]$CliExtraArgsJson
    )

    $inv = $MyInvocation.MyCommand.Name
    $deleteTempZip = $false

    try
    {
        # ── If $ZipPath is a folder, compress it into a temporary zip file ──────────────
        if (Test-Path $ZipPath -PathType Container)
        {
            $tempZip = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName() + ".zip")
            # Compress the folder's contents (all items inside the folder)
            Compress-Archive -Path (Join-Path (Resolve-Path $ZipPath).Path "*") -DestinationPath $tempZip -Force
            _LogMessage -Level INFO -Message "Folder '$ZipPath' compressed to zip archive '$tempZip'." -InvocationName $inv
            $ZipPath = $tempZip
            $deleteTempZip = $true
        }

        # ── Parse extra CLI arguments from JSON, if supplied ───────────────────────────
        $extraArgs = @()
        if ($CliExtraArgsJson)
        {
            try
            {
                $jsonArgs = $CliExtraArgsJson | ConvertFrom-Json
                if ($jsonArgs -isnot [System.Collections.IEnumerable])
                {
                    throw "CliExtraArgsJson must deserialize to an array."
                }
                $extraArgs = [string[]]$jsonArgs
            }
            catch
            {
                _LogMessage -Level ERROR -Message "CliExtraArgsJson invalid: $( $_.Exception.Message )" -InvocationName $inv
                throw
            }
        }

        # ── Build the az CLI command ─────────────────────────────────────────────
        $cli = @(
            'functionapp', 'deployment', 'source', 'config-zip',
            '--resource-group', $ResourceGroup,
            '--name', $FunctionAppName,
            '--src', (Resolve-Path $ZipPath).Path
        )
        if ($extraArgs)
        {
            $cli += $extraArgs
        }

        _LogMessage -Level INFO -Message "az $( $cli -join ' ' )" -InvocationName $inv
        az @cli | Out-Null
        _LogMessage -Level DEBUG -Message "az exit-code: $LASTEXITCODE" -InvocationName $inv

        if ($LASTEXITCODE)
        {
            throw "Deployment failed on $FunctionAppName (exit $LASTEXITCODE)."
        }

        # ── Optionally restart the function app ─────────────────────────────────────
        if ($RestartOnFinish)
        {
            $restart = @(
                'functionapp', 'restart',
                '--resource-group', $ResourceGroup,
                '--name', $FunctionAppName
            )

            _LogMessage -Level INFO -Message "Restarting Function App $FunctionAppName..." -InvocationName $inv
            az @restart | Out-Null
            if ($LASTEXITCODE)
            {
                _LogMessage -Level WARN -Message "Restart on $FunctionAppName returned exit-code $LASTEXITCODE." -InvocationName $inv
            }
        }

        _LogMessage -Level INFO -Message "Deployment completed successfully on $FunctionAppName" -InvocationName $inv
    }
    catch
    {
        _LogMessage -Level ERROR -Message $_.Exception.Message -InvocationName $inv
        throw
    }
    finally
    {
        # Clean up the temporary zip if we created one
        if ($deleteTempZip -and (Test-Path $ZipPath))
        {
            Remove-Item $ZipPath -Force -ErrorAction SilentlyContinue
            _LogMessage -Level INFO -Message "Temporary zip file removed." -InvocationName $inv
        }
    }
}


function Get-FunctionAppDefaultUrl
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ResourceGroup,
        [Parameter(Mandatory)][string]$FunctionAppName
    )

    $inv = $MyInvocation.MyCommand.Name
    try
    {
        $hostName = az functionapp show `
                    --resource-group $ResourceGroup `
                    --name $FunctionAppName `
                    --query "defaultHostName" -o tsv

        if (-not $hostName)
        {
            throw 'Unable to retrieve default host name.'
        }

        $url = "https://$hostName"
        _LogMessage -Level INFO -Message "Function App default URL: $url" -InvocationName $inv
        return $url
    }
    catch
    {
        _LogMessage -Level ERROR -Message $_.Exception.Message -InvocationName $inv
        throw
    }
}

function Invoke-FunctionAppSetSettings
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ResourceGroup,
        [Parameter(Mandatory)][string]$FunctionAppName,

    # Either a JSON string **or** a path to a *.json* file
        [Parameter(Mandatory)][string]$SettingsJsonOrPath
    )

    $inv = $MyInvocation.MyCommand.Name
    try
    {
        # ── Resolve JSON ----------------------------------------------------
        if (Test-Path $SettingsJsonOrPath -PathType Leaf)
        {
            $json = Get-Content $SettingsJsonOrPath -Raw
            _LogMessage -Level DEBUG -Message "Using JSON from file: $SettingsJsonOrPath" -InvocationName $inv
        }
        else
        {
            $json = $SettingsJsonOrPath
            _LogMessage -Level DEBUG -Message "Using in-memory JSON string."     -InvocationName $inv
        }

        # Validate that it *is* JSON and looks like an object
        try
        {
            $null = $json | ConvertFrom-Json
        }
        catch
        {
            throw "SettingsJsonOrPath does not contain valid JSON. $( $_.Exception.Message )"
        }

        # ── Persist to a temp file -----------------------------------------
        $tmp = New-TemporaryFile
        Set-Content -Path $tmp -Value $json -Encoding UTF8
        _LogMessage -Level INFO -Message "Temp settings file → $tmp" -InvocationName $inv

        # ── az call ---------------------------------------------------------
        $cli = @(
            'functionapp', 'config', 'appsettings', 'set',
            '--resource-group', $ResourceGroup,
            '--name', $FunctionAppName,
            '--settings', "@$tmp"
        )
        _LogMessage -Level INFO -Message "az $( $cli -join ' ' )" -InvocationName $inv
        az @cli | Out-Null
        if ($LASTEXITCODE)
        {
            throw "az returned exit-code $LASTEXITCODE."
        }

        _LogMessage -Level INFO -Message "$FunctionAppName App-settings updated successfully." -InvocationName $inv
    }
    catch
    {
        _LogMessage -Level ERROR -Message $_.Exception.Message -InvocationName $inv
        throw
    }
    finally
    {
        if (Test-Path $tmp)
        {
            Remove-Item $tmp -ErrorAction SilentlyContinue
        }
    }
}

function Set-CurrentIPInFuncAccess
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ResourceGroup,
        [Parameter(Mandatory)][string]$FunctionAppName,
        [Parameter(Mandatory)][bool]  $AddRule,

        [string]$RuleName = 'AllowCurrentIp',
        [int]   $Priority = 1000,
        [string]$Action = 'Allow')

    $inv = $MyInvocation.MyCommand.Name
    try
    {
        if ($AddRule)
        {
            # ── 1. discover caller IP ─────────────────────────────────────
            $currentIp = (Invoke-RestMethod -Uri 'https://checkip.amazonaws.com').Trim()
            if (-not $currentIp)
            {
                _LogMessage -Level ERROR -Message 'Failed to obtain public IP.' -InvocationName $inv
                return
            }
            _LogMessage -Level INFO -Message "Current IP: $currentIp" -InvocationName $inv

            # We need this as per https://github.com/Azure/azure-cli/issues/24947
            az functionapp update `
            -g $ResourceGroup -n $FunctionAppName `
            --set publicNetworkAccess=Enabled siteConfig.publicNetworkAccess=Enabled `
            --query "{name:name, publicNetworkAccess:publicNetworkAccess, siteConfig_publicNetworkAccess:siteConfig.publicNetworkAccess}" | Out-Null

            az functionapp config access-restriction set `
                 -g $ResourceGroup -n $FunctionAppName `
                 --default-action $Action `
                 --scm-default-action $Action `
                 --use-same-restrictions-for-scm-site true | Out-Null


            # ── 3. add rule (main site) ──────────────────────────────────
            az functionapp config access-restriction add `
                 -g $ResourceGroup -n $FunctionAppName `
                 --rule-name $RuleName --action $Action `
                 --priority $Priority --ip-address $currentIp | Out-Null



            _LogMessage -Level INFO -Message "Access rule '$RuleName' added for IP $currentIp to $FunctionAppName" -InvocationName $inv
        }
        else
        {
            # ── 1. remove rule(s) if present ─────────────────────────────
            az functionapp config access-restriction remove `
                 -g $ResourceGroup -n $FunctionAppName `
                 --rule-name $RuleName  | Out-Null

            # ── 2. disable restrictions (default-action = Allow) ────────
            az functionapp config access-restriction set `
                 -g $ResourceGroup -n $FunctionAppName `
                 --default-action Deny `
                 --scm-default-action Deny `
                 --use-same-restrictions-for-scm-site true | Out-Null

            az functionapp update `
            -g $ResourceGroup -n $FunctionAppName `
            --set publicNetworkAccess=Disabled siteConfig.publicNetworkAccess=Disabled `
            --query "{name:name, publicNetworkAccess:publicNetworkAccess, siteConfig_publicNetworkAccess:siteConfig.publicNetworkAccess}" | Out-Null

            _LogMessage -Level INFO -Message "Access rule '$RuleName' removed and public access re-enabled in $FunctionAppName" -InvocationName $inv
        }

        _LogMessage -Level INFO -Message "Function-app - $FunctionAppName - access-restriction update complete." -InvocationName $inv
    }
    catch
    {
        _LogMessage -Level ERROR -Message "An error occurred: $_" -InvocationName $inv
        throw
    }
}


Export-ModuleMember -Function `
    Compress-FunctionAppSource, `
      Invoke-FunctionAppDeployZip, `
      Get-FunctionAppDefaultUrl, `
      Invoke-FunctionAppSetSettings, `
      Set-CurrentIPInFuncAccess