LibreDevOpsHelpers.TerraformDocs/LibreDevOpsHelpers.TerraformDocs.psm1

Set-StrictMode -Version Latest

function Format-LdoTerraform {
    <#
    .SYNOPSIS
        Runs 'terraform fmt -recursive' against a configuration folder.

    .DESCRIPTION
        Confirms terraform is on PATH, then formats all Terraform files beneath the folder.
        Throws on failure. The original working directory is always restored.

    .PARAMETER CodePath
        Path to the Terraform configuration folder.

    .EXAMPLE
        Format-LdoTerraform -CodePath ./terraform

    .OUTPUTS
        None
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$CodePath
    )

    $orig = Get-Location
    try {
        $tf = Get-Command terraform -ErrorAction Stop
        Write-LdoLog -Level INFO -Message "terraform found at '$($tf.Source)'"
        Set-Location $CodePath
        & terraform fmt -recursive
        if ($LASTEXITCODE -ne 0) {
            throw "terraform fmt returned exit code $LASTEXITCODE."
        }
        Write-LdoLog -Level INFO -Message 'Terraform files formatted (fmt -recursive).'
    }
    finally {
        Set-Location $orig
    }
}

function Get-LdoTerraformFileContent {
    <#
    .SYNOPSIS
        Reads a Terraform file and returns its raw content.

    .DESCRIPTION
        Returns the full text of a file, throwing when the file does not exist.

    .PARAMETER Filename
        Path to the file to read.

    .EXAMPLE
        Get-LdoTerraformFileContent -Filename ./variables.tf

    .OUTPUTS
        System.String
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$Filename)

    if (-not (Test-Path $Filename)) {
        throw "File not found: $Filename"
    }
    return Get-Content -Raw -LiteralPath $Filename
}

function Set-LdoTerraformFileContent {
    <#
    .SYNOPSIS
        Writes content to a Terraform file.

    .DESCRIPTION
        Overwrites the named file with the supplied content.

    .PARAMETER Filename
        Path to the file to write.

    .PARAMETER Content
        Text content to write.

    .EXAMPLE
        Set-LdoTerraformFileContent -Filename ./variables.tf -Content $text

    .OUTPUTS
        None
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$Filename,
        [Parameter(Mandatory)][AllowEmptyString()][string]$Content
    )

    $Content | Set-Content -LiteralPath $Filename
}

function Format-LdoTerraformVariables {
    <#
    .SYNOPSIS
        Sorts variable blocks in variables.tf content alphabetically.

    .DESCRIPTION
        Parses variable "name" { ... } blocks from the supplied content and returns them sorted
        by variable name, separated by blank lines.

    .PARAMETER VariablesContent
        Raw content of a variables.tf file.

    .EXAMPLE
        Format-LdoTerraformVariables -VariablesContent (Get-Content ./variables.tf -Raw)

    .OUTPUTS
        System.String
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingularNouns', '', Justification = 'Operates on Terraform variable blocks.')]
    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$VariablesContent)

    $pattern = 'variable\s+"[^"]+"\s+\{[\s\S]*?\n\}'
    $blocks = [regex]::Matches($VariablesContent, $pattern) | ForEach-Object { $_.Value }
    $sorted = $blocks | Sort-Object { ([regex]::Match($_, 'variable\s+"([^"]+)"')).Groups[1].Value }
    return ($sorted -join "`n`n")
}

function Format-LdoTerraformOutputs {
    <#
    .SYNOPSIS
        Sorts output blocks in outputs.tf content alphabetically.

    .DESCRIPTION
        Parses output "name" { ... } blocks from the supplied content and returns them sorted by
        output name, separated by blank lines.

    .PARAMETER OutputsContent
        Raw content of an outputs.tf file.

    .EXAMPLE
        Format-LdoTerraformOutputs -OutputsContent (Get-Content ./outputs.tf -Raw)

    .OUTPUTS
        System.String
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingularNouns', '', Justification = 'Operates on Terraform output blocks.')]
    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$OutputsContent)

    $pattern = 'output\s+"[^"]+"\s+\{[\s\S]*?\n\}'
    $blocks = [regex]::Matches($OutputsContent, $pattern) | ForEach-Object { $_.Value }
    $sorted = $blocks | Sort-Object { ([regex]::Match($_, 'output\s+"([^"]+)"')).Groups[1].Value }
    return ($sorted -join "`n`n")
}

function Format-LdoTerraformCode {
    <#
    .SYNOPSIS
        Formats Terraform code and alphabetises variables.tf and outputs.tf.

    .DESCRIPTION
        Runs terraform fmt -recursive, then sorts the variable and output blocks in the named
        files (when present and non-empty) so the declarations are kept in a consistent order.

    .PARAMETER CodePath
        Path to the Terraform configuration folder.

    .PARAMETER VariablesFile
        Variables file name within the folder. Defaults to variables.tf.

    .PARAMETER OutputsFile
        Outputs file name within the folder. Defaults to outputs.tf.

    .EXAMPLE
        Format-LdoTerraformCode -CodePath ./terraform

    .OUTPUTS
        None
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$CodePath,
        [string]$VariablesFile = 'variables.tf',
        [string]$OutputsFile = 'outputs.tf'
    )

    Format-LdoTerraform -CodePath $CodePath

    $varPath = Join-Path $CodePath $VariablesFile
    if (Test-Path $varPath) {
        $varsContent = Get-LdoTerraformFileContent -Filename $varPath
        if (-not [string]::IsNullOrWhiteSpace($varsContent)) {
            $sortedVars = Format-LdoTerraformVariables -VariablesContent $varsContent
            if (-not [string]::IsNullOrWhiteSpace($sortedVars)) {
                Set-LdoTerraformFileContent -Filename $varPath -Content $sortedVars
                Write-LdoLog -Level INFO -Message "Sorted variables in $varPath"
            }
            else {
                Write-LdoLog -Level INFO -Message "No variable blocks found to sort in $varPath; skipping write."
            }
        }
        else {
            Write-LdoLog -Level INFO -Message "File $varPath is empty; skipping variable sort."
        }
    }

    $outPath = Join-Path $CodePath $OutputsFile
    if (Test-Path $outPath) {
        $outContent = Get-LdoTerraformFileContent -Filename $outPath
        if (-not [string]::IsNullOrWhiteSpace($outContent)) {
            $sortedOut = Format-LdoTerraformOutputs -OutputsContent $outContent
            if (-not [string]::IsNullOrWhiteSpace($sortedOut)) {
                Set-LdoTerraformFileContent -Filename $outPath -Content $sortedOut
                Write-LdoLog -Level INFO -Message "Sorted outputs in $outPath"
            }
            else {
                Write-LdoLog -Level INFO -Message "No output blocks found to sort in $outPath; skipping write."
            }
        }
        else {
            Write-LdoLog -Level INFO -Message "File $outPath is empty; skipping output sort."
        }
    }
}

function Update-LdoReadmeWithTerraformDocs {
    <#
    .SYNOPSIS
        Regenerates README.md for a Terraform folder using terraform-docs.

    .DESCRIPTION
        Writes the chosen build file (build.tf or main.tf) into a fenced HCL block at the top of
        the README, then appends the terraform-docs markdown table. Skips silently when
        terraform-docs is not installed or no build file is present. The original working
        directory is always restored.

    .PARAMETER CodePath
        Path to the Terraform configuration folder.

    .PARAMETER ReadmeFile
        README file name within the folder. Defaults to README.md.

    .EXAMPLE
        Update-LdoReadmeWithTerraformDocs -CodePath ./terraform

    .OUTPUTS
        None
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingularNouns', '', Justification = 'terraform-docs is a tool name.')]
    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$CodePath,
        [string]$ReadmeFile = 'README.md'
    )

    $orig = Get-Location
    try {
        try {
            $td = Get-Command terraform-docs -ErrorAction Stop
            Write-LdoLog -Level INFO -Message "terraform-docs found at '$($td.Source)'"
        }
        catch {
            Write-LdoLog -Level WARN -Message 'terraform-docs not installed; README generation skipped.'
            return
        }

        Set-Location $CodePath

        $build = @('build.tf', 'main.tf') | Where-Object { Test-Path $_ } | Select-Object -First 1
        if (-not $build) {
            Write-LdoLog -Level WARN -Message 'No build.tf or main.tf found; README not updated.'
            return
        }

        Write-LdoLog -Level INFO -Message "Generating $ReadmeFile from $build and terraform-docs."

        '```hcl' | Set-Content $ReadmeFile
        Get-Content $build | Add-Content $ReadmeFile
        '```' | Add-Content $ReadmeFile
        terraform-docs markdown . | Add-Content $ReadmeFile

        Write-LdoLog -Level SUCCESS -Message "Updated $ReadmeFile."
    }
    finally {
        Set-Location $orig
    }
}

Export-ModuleMember -Function `
    Format-LdoTerraform, `
    Format-LdoTerraformCode, `
    Get-LdoTerraformFileContent, `
    Set-LdoTerraformFileContent, `
    Format-LdoTerraformVariables, `
    Format-LdoTerraformOutputs, `
    Update-LdoReadmeWithTerraformDocs