TfPlanVisualizer.psm1

#requires -version 7.0
<#
.SYNOPSIS
    TfPlanVisualizer module — renders Terraform plans as self-contained HTML reports.

.DESCRIPTION
    Module root script. Dot-sources the internal parser (ConvertTo-PlanReportModel)
    at import time and exports New-TerraformPlanReport as the sole public cmdlet.
#>


$ErrorActionPreference = 'Stop'

$script:LibPath = Join-Path -Path $PSScriptRoot -ChildPath 'lib'
$script:ParserPath = Join-Path -Path $script:LibPath -ChildPath 'Parse-TerraformPlan.ps1'
$script:TemplatePath = Join-Path -Path $script:LibPath -ChildPath 'template.html'

if (-not (Test-Path -LiteralPath $script:ParserPath)) {
    throw "Parser script not found at '$script:ParserPath'."
}

# Dot-source at module load time so ConvertTo-PlanReportModel is available to
# New-TerraformPlanReport below, but stays private (not exported).
. $script:ParserPath

function New-TerraformPlanReport {
    <#
    .SYNOPSIS
        Renders a `terraform show -json` plan as a single self-contained dark-themed
        HTML report and opens it in the default browser.

    .DESCRIPTION
        Local dev tool — no server, no build step. Supply either a saved binary
        Terraform plan file (`-PlanFile`, this cmdlet runs `terraform show -json`
        itself) or an already-exported `terraform show -json` file (`-PlanJson`).
        The plan is normalized by lib/Parse-TerraformPlan.ps1 and injected into
        lib/template.html in place of the __REPORT_DATA__ token.

    .PARAMETER PlanFile
        Path to a saved binary Terraform plan file (e.g. produced by
        `terraform plan -out=tfplan`). Mutually exclusive with -PlanJson.

    .PARAMETER PlanJson
        Path to already-exported `terraform show -json` output. Mutually
        exclusive with -PlanFile.

    .PARAMETER OutputPath
        Path to write the generated HTML report to. Defaults to
        ./terraform-plan-report.html (relative to the caller's current directory).

    .PARAMETER NoOpen
        Skip opening the generated report in the default browser.

    .EXAMPLE
        New-TerraformPlanReport -PlanFile ./tfplan

    .EXAMPLE
        terraform show -json ./tfplan > plan.json
        New-TerraformPlanReport -PlanJson ./plan.json -OutputPath ./report.html -NoOpen
    #>

    [CmdletBinding(DefaultParameterSetName = 'FromPlanFile')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'FromPlanFile')]
        [ValidateNotNullOrEmpty()]
        [string]
        $PlanFile,

        [Parameter(Mandatory, ParameterSetName = 'FromPlanJson')]
        [ValidateNotNullOrEmpty()]
        [string]
        $PlanJson,

        [Parameter()]
        [string]
        $OutputPath = './terraform-plan-report.html',

        [Parameter()]
        [switch]
        $NoOpen
    )

    $parserPath = $script:ParserPath
    $templatePath = $script:TemplatePath

    if (-not (Test-Path -LiteralPath $parserPath)) {
        Write-Error "Parser script not found at '$parserPath'."
        return
    }
    if (-not (Test-Path -LiteralPath $templatePath)) {
        Write-Error "HTML template not found at '$templatePath'."
        return
    }

    # --- Obtain raw `terraform show -json` output as a string -----------------

    $rawJson = $null

    if ($PSCmdlet.ParameterSetName -eq 'FromPlanFile') {
        if (-not (Test-Path -LiteralPath $PlanFile)) {
            Write-Error "Plan file not found: '$PlanFile'."
            return
        }

        $terraformCmd = Get-Command -Name 'terraform' -ErrorAction SilentlyContinue
        if (-not $terraformCmd) {
            Write-Error "'terraform' was not found on PATH. Install Terraform, or export the plan yourself with 'terraform show -json <planfile> > plan.json' and pass it via -PlanJson instead."
            return
        }

        try {
            $resolvedPlanFile = (Resolve-Path -LiteralPath $PlanFile).Path
            $rawJson = & $terraformCmd.Source show -json $resolvedPlanFile 2>&1

            if ($LASTEXITCODE -ne 0) {
                Write-Error "'terraform show -json' exited with code $LASTEXITCODE for plan file '$PlanFile':`n$rawJson"
                return
            }

            $rawJson = $rawJson -join [Environment]::NewLine
        }
        catch {
            Write-Error "Failed to run 'terraform show -json' against '$PlanFile': $($_.Exception.Message)"
            return
        }
    }
    else {
        if (-not (Test-Path -LiteralPath $PlanJson)) {
            Write-Error "Plan JSON file not found: '$PlanJson'."
            return
        }

        try {
            $rawJson = Get-Content -LiteralPath $PlanJson -Raw
        }
        catch {
            Write-Error "Failed to read plan JSON from '$PlanJson': $($_.Exception.Message)"
            return
        }
    }

    # --- Parse and normalize ----------------------------------------------------

    $planObject = $null
    try {
        $planObject = $rawJson | ConvertFrom-Json -Depth 100
    }
    catch {
        Write-Error "Failed to parse plan JSON: $($_.Exception.Message)"
        return
    }

    $reportModel = $null
    try {
        $reportModel = ConvertTo-PlanReportModel -PlanJson $planObject
    }
    catch {
        Write-Error "Failed to normalize plan JSON into a report model: $($_.Exception.Message)"
        return
    }

    $reportModelJson = $null
    try {
        $reportModelJson = $reportModel | ConvertTo-Json -Depth 20 -Compress
    }
    catch {
        Write-Error "Failed to serialize report model: $($_.Exception.Message)"
        return
    }

    # --- Inject into template and write output ----------------------------------

    $templateContent = $null
    try {
        $templateContent = Get-Content -LiteralPath $templatePath -Raw
    }
    catch {
        Write-Error "Failed to read template from '$templatePath': $($_.Exception.Message)"
        return
    }

    if ($templateContent -notmatch [regex]::Escape('__REPORT_DATA__')) {
        Write-Error "Template at '$templatePath' does not contain the expected '__REPORT_DATA__' placeholder."
        return
    }

    $reportHtml = $templateContent.Replace('__REPORT_DATA__', $reportModelJson)

    try {
        Set-Content -LiteralPath $OutputPath -Value $reportHtml -NoNewline -Encoding utf8
    }
    catch {
        Write-Error "Failed to write report to '$OutputPath': $($_.Exception.Message)"
        return
    }

    $resolvedOutput = (Resolve-Path -LiteralPath $OutputPath).Path
    Write-Host "Report written to: $resolvedOutput"

    if (-not $NoOpen) {
        try {
            if ($IsMacOS) {
                & open $resolvedOutput
            }
            elseif ($IsWindows) {
                Start-Process $resolvedOutput
            }
            elseif ($IsLinux) {
                & xdg-open $resolvedOutput
            }
            else {
                Start-Process $resolvedOutput
            }
        }
        catch {
            Write-Error "Report was written successfully but could not be opened automatically: $($_.Exception.Message)"
        }
    }
}

Export-ModuleMember -Function New-TerraformPlanReport