#requires -Version 7.2
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [ValidatePattern('^[0-9a-fA-F-]{36}$')]
    [string]$SubscriptionId,

    [Parameter(Mandatory)]
    [ValidatePattern('^/subscriptions/[0-9a-fA-F-]{36}/resourceGroups/rg-machine-configuration-deep-dive-we/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-machine-config-package-we$')]
    [string]$ManagedIdentityResourceId,

    [Parameter(Mandatory)]
    [ValidatePattern('^https://[a-z0-9]{3,24}\.blob\.core\.windows\.net/machine-configuration/WorkshopWindowsBaseline-[0-9.]+-(Audit|AuditAndSet)\.zip$')]
    [string]$AuditContentUri,

    [Parameter(Mandatory)]
    [ValidatePattern('^https://[a-z0-9]{3,24}\.blob\.core\.windows\.net/machine-configuration/WorkshopWindowsBaseline-[0-9.]+-(Audit|AuditAndSet)\.zip$')]
    [string]$EnforceContentUri,

    [switch]$SkipLocalAudit
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

$root = Split-Path -Parent $PSScriptRoot
$sourcePath = Join-Path $PSScriptRoot 'WorkshopWindowsBaseline.ps1'
$moduleRoot = Join-Path $PSScriptRoot 'Modules'
$buildRoot = Join-Path $root '.private\package-build'
$packageRoot = Join-Path $root 'packages'
$policyRoot = Join-Path $root 'policies\generated'
$transcriptPath = Join-Path $root 'validation-transcript.md'

function Protect-Text {
    param([AllowNull()][string]$Text)
    if ($null -eq $Text) { return '' }
    $safe = $Text -replace [regex]::Escape($SubscriptionId), '<subscription-redacted>'
    $safe = $safe -replace '(?i)/subscriptions/[0-9a-f-]{36}', '/subscriptions/<redacted>'
    $safe = $safe -replace '(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b', '<guid-redacted>'
    $safe = $safe -replace '(?i)https://[a-z0-9]{3,24}\.blob\.core\.windows\.net', 'https://<storage-redacted>.blob.core.windows.net'
    return $safe
}

function Write-Lab {
    param([string]$Message, [ValidateSet('INFO', 'PASS', 'WARN')][string]$Level = 'INFO')
    $line = '[{0}] {1}' -f $Level, (Protect-Text $Message)
    Write-Host $line
    if (-not (Test-Path -LiteralPath $transcriptPath)) { "# Sanitized validation transcript`n" | Out-File -LiteralPath $transcriptPath -Encoding utf8 }
    "- ``$line``" | Out-File -LiteralPath $transcriptPath -Append -Encoding utf8
}

foreach ($module in @(
    @{ Name = 'GuestConfiguration'; Version = '4.12.0' },
    @{ Name = 'PSDesiredStateConfiguration'; Version = '2.0.7' },
    @{ Name = 'PSDscResources'; Version = '2.12.0.0' }
)) {
    $found = Get-Module -ListAvailable -Name $module.Name | Where-Object Version -eq $module.Version | Select-Object -First 1
    if (-not $found) { throw "Required module $($module.Name) $($module.Version) is not installed." }
}

Import-Module GuestConfiguration -RequiredVersion 4.12.0 -Force
Import-Module PSDesiredStateConfiguration -RequiredVersion 2.0.7 -Force
Import-Module PSDscResources -RequiredVersion 2.12.0.0 -Force
$env:PSModulePath = "$moduleRoot$([IO.Path]::PathSeparator)$env:PSModulePath"
Import-Module WorkshopMachineConfiguration -RequiredVersion 1.0.0 -Force

New-Item -ItemType Directory -Path $buildRoot, $packageRoot, $policyRoot -Force | Out-Null
Remove-Item -LiteralPath $buildRoot -Recurse -Force
New-Item -ItemType Directory -Path $buildRoot -Force | Out-Null
if (Test-Path -LiteralPath $policyRoot) { Remove-Item -LiteralPath $policyRoot -Recurse -Force }
New-Item -ItemType Directory -Path $policyRoot -Force | Out-Null

. $sourcePath
$compilePath = Join-Path $buildRoot 'WorkshopWindowsBaseline'
WorkshopWindowsBaseline -OutputPath $compilePath -IisEnsure Absent | Out-Null
$mofPath = Join-Path $compilePath 'localhost.mof'
if (-not (Test-Path -LiteralPath $mofPath)) { throw 'DSC compilation did not produce localhost.mof.' }

$mofText = Get-Content -Raw -LiteralPath $mofPath
if ($mofText -notmatch 'ConfigurationName = "WorkshopWindowsBaseline"') { throw 'The MOF configuration name is not WorkshopWindowsBaseline.' }
Write-Lab 'Compiled WorkshopWindowsBaseline into a MOF with the expected configuration name.' PASS

$badPackageResult = $null
try {
    $badPackageResult = New-GuestConfigurationPackage -Name 'IntentionalNameMismatch' -Configuration (Get-Item $mofPath) -Version '0.0.1' -Type Audit -Path (Get-Item $buildRoot) -Force -ErrorAction Stop
    $badZip = [string]$badPackageResult.Path
    $badExpanded = Join-Path $buildRoot 'bad-name-expanded'
    Expand-Archive -LiteralPath $badZip -DestinationPath $badExpanded -Force
    $badMeta = Get-ChildItem -LiteralPath $badExpanded -Filter '*.json' -Recurse | Where-Object Name -Match 'metaconfig' | Select-Object -First 1
    if ($badMeta) { Write-Lab 'Intentional package-name mismatch was created for the troubleshooting demonstration.' WARN }
}
catch {
    Write-Lab "Expected package-name mismatch failure: $($_.Exception.Message)" WARN
}

$auditOutput = New-GuestConfigurationPackage -Name 'WorkshopWindowsBaseline' -Configuration (Get-Item $mofPath) -Version '1.0.0' -Type Audit -FrequencyMinutes 15 -Path (Get-Item $buildRoot) -Force
$auditPath = Join-Path $packageRoot 'WorkshopWindowsBaseline-1.0.0-Audit.zip'
Copy-Item -LiteralPath $auditOutput.Path -Destination $auditPath -Force

$enforceOutput = New-GuestConfigurationPackage -Name 'WorkshopWindowsBaseline' -Configuration (Get-Item $mofPath) -Version '1.1.0' -Type AuditAndSet -FrequencyMinutes 15 -Path (Get-Item $buildRoot) -Force
$enforcePath = Join-Path $packageRoot 'WorkshopWindowsBaseline-1.1.0-AuditAndSet.zip'
Copy-Item -LiteralPath $enforceOutput.Path -Destination $enforcePath -Force

foreach ($package in @($auditPath, $enforcePath)) {
    $expanded = Join-Path $buildRoot ([IO.Path]::GetFileNameWithoutExtension($package))
    Expand-Archive -LiteralPath $package -DestinationPath $expanded -Force
    $size = (Get-ChildItem -LiteralPath $expanded -Recurse -File | Measure-Object Length -Sum).Sum
    if ($size -ge 100MB) { throw "Package $package exceeds the 100 MB uncompressed limit." }
    Write-Lab "Validated package archive $([IO.Path]::GetFileName($package)); uncompressed size $([math]::Round($size / 1MB, 2)) MB." PASS
}

if (-not $SkipLocalAudit) {
    $principal = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent())
    if ($principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        $parameters = @(@{
            ResourceType          = 'WorkshopWindowsFeature'
            ResourceId            = 'IisWebServer'
            ResourcePropertyName  = 'Ensure'
            ResourcePropertyValue = 'Absent'
        })
        $localResult = Get-GuestConfigurationPackageComplianceStatus -Path (Get-Item $auditPath) -Parameter $parameters
        Write-Lab "Local non-mutating package test completed; compliance result: $($localResult.complianceStatus)." PASS
    }
    else {
        Write-Lab 'The local audit test requires an elevated authoring shell; package execution will be validated through Azure VM Run Command.' WARN
    }
}

$policyParameter = @(@{
    Name                 = 'IisEnsure'
    DisplayName          = 'IIS desired state'
    Description          = 'Present for the application role and Absent for the baseline role.'
    ResourceType         = 'WorkshopWindowsFeature'
    ResourceId           = 'IisWebServer'
    ResourcePropertyName = 'Ensure'
    AllowedValues        = @('Present', 'Absent')
})

$roles = @(
    @{ Key = 'baseline'; Display = 'Windows baseline'; Role = 'Baseline'; Iis = 'Absent' },
    @{ Key = 'application'; Display = 'Windows application server'; Role = 'Application'; Iis = 'Present' }
)

$policyManifest = [ordered]@{ Audit = @(); Enforce = @() }
foreach ($role in $roles) {
    $auditDir = Join-Path $policyRoot "audit-$($role.Key)"
    $enforceDir = Join-Path $policyRoot "enforce-$($role.Key)"
    New-Item -ItemType Directory -Path $auditDir, $enforceDir -Force | Out-Null

    $auditId = [guid]::NewGuid()
    $enforceId = [guid]::NewGuid()
    $auditParam = @{} + $policyParameter[0]
    $auditParam.DefaultValue = $role.Iis
    $enforceParam = @{} + $policyParameter[0]
    $enforceParam.DefaultValue = $role.Iis

    $generationErrors = @()
    New-GuestConfigurationPolicy -PolicyId $auditId -ContentUri $AuditContentUri -LocalContentPath $auditPath -ManagedIdentityResourceId $ManagedIdentityResourceId -DisplayName "Audit Azure Workshopz $($role.Display) configuration" -Description "Audits the WorkshopWindowsBaseline DSC package for $($role.Display.ToLowerInvariant()) machines in the Pilot ring." -PolicyVersion '1.0.0' -ContentVersion '1.0.0' -Path $auditDir -Platform Windows -Parameter @($auditParam) -Mode Audit -Tag @{ ConfigurationRing = 'Pilot'; ServerRole = $role.Role } -IncludeVMSS:$false -ExcludeArcMachines -ErrorAction SilentlyContinue -ErrorVariable +generationErrors | Out-Null
    New-GuestConfigurationPolicy -PolicyId $enforceId -ContentUri $EnforceContentUri -LocalContentPath $enforcePath -ManagedIdentityResourceId $ManagedIdentityResourceId -DisplayName "Enforce Azure Workshopz $($role.Display) configuration" -Description "Applies and continuously corrects the WorkshopWindowsBaseline DSC package for $($role.Display.ToLowerInvariant()) machines in the Pilot ring." -PolicyVersion '1.1.0' -ContentVersion '1.1.0' -Path $enforceDir -Platform Windows -Parameter @($enforceParam) -Mode ApplyAndAutoCorrect -Tag @{ ConfigurationRing = 'Pilot'; ServerRole = $role.Role } -IncludeVMSS:$false -ExcludeArcMachines -ErrorAction SilentlyContinue -ErrorVariable +generationErrors | Out-Null
    $auditPolicyPath = Join-Path $auditDir 'WorkshopWindowsBaseline_AuditIfNotExists.json'
    $enforcePolicyPath = Join-Path $enforceDir 'WorkshopWindowsBaseline_DeployIfNotExists.json'

    # GuestConfiguration 4.12.0 emits EnableAutoRemediation=false and omits the
    # LCM configurationSetting even when Mode is ApplyAndAutoCorrect. Make both
    # enforcement controls explicit so existing VMs apply and continuously
    # correct the package after the DeployIfNotExists remediation runs.
    $enforcePolicy = Get-Content -Raw -LiteralPath $enforcePolicyPath | ConvertFrom-Json -Depth 100
    $enforcePolicy.properties.parameters.EnableAutoRemediation.defaultValue = 'true'
    $enforceGuestConfiguration = $enforcePolicy.properties.policyRule.then.details.deployment.properties.template.resources[0].properties.guestConfiguration
    $enforceGuestConfiguration | Add-Member -NotePropertyName configurationSetting -NotePropertyValue ([ordered]@{
        configurationMode              = 'ApplyAndAutoCorrect'
        configurationModeFrequencyMins = 15
        refreshFrequencyMins           = 5
        allowModuleOverwrite           = $true
        actionAfterReboot              = 'ContinueConfiguration'
        rebootIfNeeded                 = $false
    }) -Force
    $enforcePolicy | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $enforcePolicyPath -Encoding utf8

    foreach ($generatedPath in @($auditPolicyPath, $enforcePolicyPath)) {
        if (-not (Test-Path -LiteralPath $generatedPath)) { throw "Policy generation did not produce $generatedPath." }
        $generated = Get-Content -Raw -LiteralPath $generatedPath | ConvertFrom-Json -Depth 100
        if (-not $generated.properties.metadata.guestConfiguration -or -not $generated.properties.policyRule.then.effect) { throw "Generated policy $generatedPath failed structural validation." }
    }
    if ($generationErrors.Count -gt 0) { Write-Lab 'GuestConfiguration 4.12.0 emitted a recoverable generator warning; all generated policy JSON passed structural validation.' WARN }

    $policyManifest.Audit += [ordered]@{ Role = $role.Role; PolicyId = $auditId.Guid; Path = $auditPolicyPath; ReferenceId = "audit-$($role.Key)" }
    $policyManifest.Enforce += [ordered]@{ Role = $role.Role; PolicyId = $enforceId.Guid; Path = $enforcePolicyPath; ReferenceId = "enforce-$($role.Key)" }
}

$manifestPath = Join-Path $root '.private\policy-manifest.json'
$policyManifest | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $manifestPath -Encoding utf8

$hashes = @(
    [ordered]@{ File = [IO.Path]::GetFileName($auditPath); Sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $auditPath).Hash.ToLowerInvariant() },
    [ordered]@{ File = [IO.Path]::GetFileName($enforcePath); Sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $enforcePath).Hash.ToLowerInvariant() }
)
$hashes | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $root '.private\package-hashes.json') -Encoding utf8

Write-Lab 'Created the Audit 1.0.0 and AuditAndSet 1.1.0 packages plus four role-aware policy definitions.' PASS
[pscustomobject]@{
    AuditPackage   = $auditPath
    EnforcePackage = $enforcePath
    PolicyManifest = $manifestPath
}
