Split-ALZ-Accelerator-exproj.txt

################################################################################
CURRENT DIRECTORY: /Users/gregc/devSandbox/github/acceleratorExtensoins/01-split-alz-accelerator/Split-ALZ-Accelerator/Split-ALZ-Accelerator
################################################################################

#################### DIRECTORY TREE ####################
RELATIVE TO: /Users/gregc/devSandbox/github/acceleratorExtensoins/01-split-alz-accelerator/Split-ALZ-Accelerator/Split-ALZ-Accelerator
.
├── private
│   ├── Invoke-AccelCleanConnectivity.ps1
│   ├── Invoke-AccelCleanEmptyLocals.ps1
│   ├── Invoke-AccelCleanManagement.ps1
│   ├── Invoke-AccelMoveFiles.ps1
│   ├── Invoke-AccelOperation.ps1
│   ├── Invoke-AccelRefactorModules.ps1
│   ├── New-AccelBackup.ps1
│   ├── New-AccelDirectory.ps1
│   ├── New-AccelSymlink.ps1
│   ├── Remove-AccelLines.ps1
│   ├── Resolve-AccelPath.ps1
│   ├── Simplify-AccelStarterLocations.ps1
│   ├── Split-AccelLandingZoneAutoTfvars.ps1
│   ├── Trim-AccelConnectivityLocals.ps1
│   ├── Update-AccelModuleSources.ps1
│   └── Update-AccelProviders.ps1
├── public
│   └── Split-Accelerator.ps1
├── Split-ALZ-Accelerator-exproj.txt
├── Split-ALZ-Accelerator.psd1
└── Split-ALZ-Accelerator.psm1

3 directories, 20 files


#################### Split-ALZ-Accelerator/Split-ALZ-Accelerator.psm1 ####################

# dot-source private helpers
Get-ChildItem -Path (Join-Path $PSScriptRoot 'Private') -Filter '*.ps1' -File -ErrorAction SilentlyContinue |
    ForEach-Object { . $_.FullName }

# dot-source public functions
Get-ChildItem -Path (Join-Path $PSScriptRoot 'Public') -Filter '*.ps1' -File -ErrorAction SilentlyContinue |
    ForEach-Object { . $_.FullName }


#################### Split-ALZ-Accelerator/Split-ALZ-Accelerator.psd1 ####################

@{
  RootModule = 'Split-ALZ-Accelerator.psm1'
  ModuleVersion = '0.3.1'
  GUID = 'b08e7c61-3c5a-46ce-8a7a-1d7f1f3d9b9f'
  Author = 'You'
  CompanyName = 'You'
  Description = 'Split an ALZ deployment into platform_* and refactor modules.'
  PowerShellVersion = '7.0'
  FunctionsToExport = @('Split-ALZ-Accelerator')
  PrivateData = @{
    PSData = @{
      Tags = @('ALZ','Terraform','Split','Accelerator')
      LicenseUri = 'https://opensource.org/licenses/MIT'
    }
  }
}


#################### Split-ALZ-Accelerator/public/Split-Accelerator.ps1 ####################

function Split-ALZ-Accelerator {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
    param(
        [Parameter(Mandatory)][string]$Path,
        [switch]$Force
    )

    Write-Verbose "Split-ALZ-Accelerator starting at $(Get-Date -Format o)"

    if ($PSCmdlet.ShouldProcess($Path, "Backup and split ALZ into platform_*; refactor modules; clean configs; fix providers")) {

        # 1) Take a backup of the ALZ directory
        try {
            $backupPath = New-AccelBackup -Path $Path -Force:$Force -Confirm:$false -WhatIf:$WhatIfPreference -Verbose:$VerbosePreference
            Write-Host "Backup created at: $backupPath"
        } catch {
            Write-Warning "Backup failed: $($_.Exception.Message)"
            # You can choose to 'return' here if you want to hard-fail when backup fails
        }

        # 2) Move files into platform_* directories, refactor modules, clean configs, fix providers, etc.
        $move = Invoke-AccelMoveFiles -Path $Path -Force:$Force -Confirm:$false -WhatIf:$WhatIfPreference -Verbose:$VerbosePreference
        $refactor = Invoke-AccelRefactorModules -Path $Path -Force:$Force -Confirm:$false -WhatIf:$WhatIfPreference -Verbose:$VerbosePreference
        $cleanConn = Invoke-AccelCleanConnectivity -Path $Path -Confirm:$false -WhatIf:$WhatIfPreference -Verbose:$VerbosePreference
        $trimConn = Trim-AccelConnectivityLocals -Path $Path -Confirm:$false -WhatIf:$WhatIfPreference -Verbose:$VerbosePreference
        $cleanMgmt = Invoke-AccelCleanManagement -Path $Path -Confirm:$false -WhatIf:$WhatIfPreference -Verbose:$VerbosePreference
        $simplOK = Simplify-AccelStarterLocations -Path $Path -Confirm:$false -WhatIf:$WhatIfPreference -Verbose:$VerbosePreference
        $fixProv = Invoke-AccelFixProviders -Path $Path -Confirm:$false -WhatIf:$WhatIfPreference -Verbose:$VerbosePreference
        $cleanEmpty = Invoke-AccelCleanEmptyLocals -Path $Path -Confirm:$false -WhatIf:$WhatIfPreference -Verbose:$VerbosePreference

        Write-Host ("Move: Moved={0} Copied={1} Deleted={2} Skipped={3}" -f $move.Moved,$move.Copied,$move.Deleted,$move.Skipped)
        Write-Host ("Refactor: RenamedModules={0} CustomModules={1} Rewritten={2} Symlinks={3} Skipped={4}" -f $refactor.ModulesRenamed,$refactor.CustomModulesReady,$refactor.FilesRewritten,$refactor.SymlinksCreated,$refactor.Skipped)
        Write-Host ("CleanConnectivity: FilesChanged={0}" -f $cleanConn)
        Write-Host ("TrimConnectivityLocals: Removed={0}" -f $trimConn)
        Write-Host ("CleanManagement: FilesChanged={0}" -f $cleanMgmt)
        Write-Host ("StarterLocationsSimplified: {0}" -f $simplOK)
        Write-Host ("FixProviders: ConnectivityUpdated={0} ManagementUpdated={1}" -f $fixProv.ConnectivityUpdated, $fixProv.ManagementUpdated)
        Write-Host ("CleanEmptyLocals: FilesChanged={0}" -f $cleanEmpty)
        return
    }
}


#################### Split-ALZ-Accelerator/private/Invoke-AccelCleanConnectivity.ps1 ####################

function Invoke-AccelCleanConnectivity {
    [CmdletBinding(SupportsShouldProcess)]
    param([Parameter(Mandatory)][string]$Path)

    $root = Resolve-AccelPath -Path $Path
    $pc = Join-Path $root 'platform_connectivity'
    if (-not (Test-Path -LiteralPath $pc -PathType Container)) { return 0 }

    $patterns = @(
        'var\.management_',
        '\bmodule\.management_resources\b'
    )
    Remove-AccelLines -Directory $pc -RegexPatterns $patterns -Confirm:$false
}


#################### Split-ALZ-Accelerator/private/Invoke-AccelCleanManagement.ps1 ####################

function Invoke-AccelCleanManagement {
    [CmdletBinding(SupportsShouldProcess)]
    param([Parameter(Mandatory)][string]$Path)

    $root = Resolve-AccelPath -Path $Path
    $pm = Join-Path $root 'platform_management'
    if (-not (Test-Path -LiteralPath $pm -PathType Container)) { return 0 }

    $patterns = @(
        '\bvar\.connectivity_type\b',
        '\bmodule\.resource_groups\b',
        '\bvar\.connectivity_resource_groups\b',
        '\bvar\.hub_and_spoke_networks_settings\b',
        '\bvar\.hub_virtual_networks\b',
        '\bvar\.virtual_wan_settings\b',
        '\bvar\.virtual_hubs\b',
        '\bvar\.connectivity_tags\b',
        '\bmodule\.hub_and_spoke_vnet\b',
        '\bmodule\.virtual_wan\b',

        '\bmodule\.config\.outputs\.(hub_and_spoke_networks_settings|hub_virtual_networks|virtual_wan_settings|virtual_hubs)\b'
    )
    Remove-AccelLines -Directory $pm -RegexPatterns $patterns -Confirm:$false
}


#################### Split-ALZ-Accelerator/private/New-AccelBackup.ps1 ####################

function New-AccelBackup {
    <#
      .SYNOPSIS
      Create a backup copy of the ALZ directory before we start moving/splitting.

      .DESCRIPTION
      Given a path to the ALZ repo root, create a sibling directory with
      a -bak, -bak1, -bak2... suffix and copy the entire tree there.

      Examples:
        C:\repos\alz-mgmt -> C:\repos\alz-mgmt-bak
        C:\repos\alz-mgmt -> C:\repos\alz-mgmt-bak1 (if -bak already exists)
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Path,
        [switch]$Force
    )

    $root = Resolve-AccelPath -Path $Path

    $parent = Split-Path -Parent -Path $root
    $name = Split-Path -Leaf -Path $root

    # base candidate: <name>-bak, then <name>-bak1, -bak2, ...
    $suffix = '-bak'
    $candidateName = "$name$suffix"
    $candidatePath = Join-Path $parent $candidateName

    $i = 1
    while (Test-Path -LiteralPath $candidatePath) {
        $candidateName = "{0}{1}{2}" -f $name, $suffix, $i
        $candidatePath = Join-Path $parent $candidateName
        $i++
    }

    if ($PSCmdlet.ShouldProcess($root, "Backup to '$candidatePath'")) {
        # Copy the whole directory tree into the backup directory
        Copy-Item -LiteralPath $root -Destination $candidatePath -Recurse -Force:$Force -ErrorAction Stop
    }

    # Return backup path for logging if needed
    return $candidatePath
}

#################### Split-ALZ-Accelerator/private/Invoke-AccelRefactorModules.ps1 ####################

function Invoke-AccelRefactorModules {
    <#
    .SYNOPSIS
    Phase 2: rename 'modules' → '_modules-accelerator', create '_modules-custom',
             rewrite module sources in .tf, and add .auto.tfvars symlinks.
    .EXAMPLE
    Invoke-AccelRefactorModules -Path '../alz-mgmt' -WhatIf -Verbose
    #>
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
    param(
        [Parameter(Mandatory)][string]$Path,
        [switch]$Force
    )

    $root = Resolve-AccelPath -Path $Path
    if (-not $PSCmdlet.ShouldProcess($root, "Refactor modules & add symlinks")) { return }

    $oldModules = Join-Path $root 'modules'
    $newModules = Join-Path $root '_modules-accelerator'
    $customMods = Join-Path $root '_modules-custom'

    $summary = [ordered]@{
        ModulesRenamed = $false
        CustomModulesReady = $false
        FilesRewritten = 0
        SymlinksCreated = 0
        Skipped = 0
    }

    try {
        $res = Invoke-AccelOperation -Action Move -Source $oldModules -Destination $newModules -Force:$Force -Confirm:$false
        if ($res -eq 'Moved' -or (Test-Path $newModules)) { $summary.ModulesRenamed = $true } else { $summary.Skipped++ }
    } catch { Write-Warning "Rename modules failed: $($_.Exception.Message)"; $summary.Skipped++ }

    try {
        New-AccelDirectory -Path $customMods -Confirm:$false | Out-Null
        $summary.CustomModulesReady = $true
    } catch { Write-Warning "Create _modules-custom failed: $($_.Exception.Message)"; $summary.Skipped++ }

    try {
        $rewritten = Update-AccelModuleSources -Root $root -Confirm:$false
        $summary.FilesRewritten = $rewritten
    } catch { Write-Warning "Update module sources failed: $($_.Exception.Message)"; $summary.Skipped++ }

    try {
        Split-AccelLandingZoneAutoTfvars -Path $root -Force:$Force -Confirm:$false -WhatIf:$WhatIfPreference -Verbose:$VerbosePreference
    } catch {
        Write-Warning "Split platform-landing-zone.auto.tfvars failed: $($_.Exception.Message)"
        $summary.Skipped++
    }

    $sharedTfvarsName = 'platform.shared.auto.tfvars'
    $platformDirs = @('platform_connectivity','platform_management') | ForEach-Object { Join-Path $root $_ }
    foreach ($pd in $platformDirs) {
        try {
            if (-not (Test-Path -LiteralPath $pd -PathType Container)) {
                Write-Verbose "Skip symlink, missing dir: $pd"
                $summary.Skipped++
                continue
            }
            $link = Join-Path $pd $sharedTfvarsName
            $target = (Join-Path '..' $sharedTfvarsName) # relative target from inside platform_* to root
            New-AccelSymlink -LinkPath $link -TargetPath $target -Force:$Force -Confirm:$false | Out-Null
            $summary.SymlinksCreated++
        }
        catch {
            Write-Warning "Symlink create failed for '$pd': $($_.Exception.Message)"
            $summary.Skipped++
        }
    }

    [pscustomobject]$summary
}


#################### Split-ALZ-Accelerator/private/Update-AccelProviders.ps1 ####################

function Update-AccelProviderInFile {
    <#
      .SYNOPSIS
      Ensure provider "azurerm" block contains the desired subscription_id assignment.
      If a subscription_id line exists, it is replaced; otherwise it's inserted after
      resource_provider_registrations (or at top of block if not found).
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$FilePath,
        [Parameter(Mandatory)][string]$SubscriptionKey # e.g. 'connectivity' or 'management'
    )

    if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) { return $false }

    $content = Get-Content -LiteralPath $FilePath -Raw -ErrorAction Stop
    $nl = ($content -match "`r`n") ? "`r`n" : "`n"

    # find start of provider "azurerm" {
    $m = [regex]::Match($content, 'provider\s+"azurerm"\s*\{', 'IgnoreCase')
    if (-not $m.Success) { return $false }

    # find the opening brace and match to its closing brace using simple brace counting
    $openIdx = $content.IndexOf('{', $m.Index)
    if ($openIdx -lt 0) { return $false }
    $i = $openIdx
    $depth = 0
    do {
        $ch = $content[$i]
        if ($ch -eq '{') { $depth++ }
        elseif ($ch -eq '}') { $depth-- }
        $i++
    } while ($i -lt $content.Length -and $depth -gt 0)
    if ($depth -ne 0) { Write-Warning "Unbalanced braces in $FilePath provider block"; return $false }

    $blockStart = $m.Index
    $blockEnd = $i - 1
    $block = $content.Substring($blockStart, $blockEnd - $blockStart + 1)

    # derive an indent from an existing property line, fall back to two spaces
    $indentMatch = [regex]::Match($block, "^\s+[A-Za-z_]+\s*=", 'Multiline')
    $indent = if ($indentMatch.Success) {
        ([regex]::Match($indentMatch.Value, '^\s+')).Value
    } else { ' ' }

    $desiredLine = $indent + 'subscription_id = var.subscription_ids["' + $SubscriptionKey + '"]'

    $blockNew = $block

    # replace existing subscription_id line if present
    $rxSubLine = [regex]::new('^\s*subscription_id\s*=\s*.*$', [System.Text.RegularExpressions.RegexOptions]::Multiline)
    if ($rxSubLine.IsMatch($blockNew)) {
        $blockNew = $rxSubLine.Replace($blockNew, [System.Text.RegularExpressions.MatchEvaluator]{ param($mm) $desiredLine })
    } else {
        # insert after resource_provider_registrations line if present
        $rxRpr = [regex]::new('^(\s*resource_provider_registrations\s*=\s*".*")\s*$', 'Multiline')
        if ($rxRpr.IsMatch($blockNew)) {
            $blockNew = $rxRpr.Replace($blockNew, { param($mm) $mm.Groups[1].Value + $nl + $desiredLine }, 1)
        } else {
            # otherwise, insert just after opening brace
            $insPos = $blockNew.IndexOf('{') + 1
            $blockNew = $blockNew.Substring(0,$insPos) + $nl + $desiredLine + $blockNew.Substring($insPos)
        }
    }

    if ($blockNew -ne $block) {
        $newContent = $content.Substring(0,$blockStart) + $blockNew + $content.Substring($blockEnd+1)
        if ($PSCmdlet.ShouldProcess($FilePath, "Set provider azurerm subscription_id = var.subscription_ids[`"$SubscriptionKey`"]")) {
            Set-Content -LiteralPath $FilePath -Value $newContent -Encoding UTF8 -ErrorAction Stop
            return $true
        }
    }

    return $false
}

function Invoke-AccelFixProviders {
    <#
      .SYNOPSIS
      Apply provider "azurerm" subscription_id fix to both platform_* terraform.tf files.
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param([Parameter(Mandatory)][string]$Path)

    $root = Resolve-AccelPath -Path $Path

    $pcFile = Join-Path (Join-Path $root 'platform_connectivity') 'terraform.tf'
    $pmFile = Join-Path (Join-Path $root 'platform_management') 'terraform.tf'

    $c = $false
    $m = $false

    try { $c = Update-AccelProviderInFile -FilePath $pcFile -SubscriptionKey 'connectivity' -Confirm:$false } catch { Write-Warning "Fix provider (connectivity) failed: $($_.Exception.Message)" }
    try { $m = Update-AccelProviderInFile -FilePath $pmFile -SubscriptionKey 'management' -Confirm:$false } catch { Write-Warning "Fix provider (management) failed: $($_.Exception.Message)" }

    # return simple tuple-like object
    [pscustomobject]@{ ConnectivityUpdated = $c; ManagementUpdated = $m }
}


#################### Split-ALZ-Accelerator/private/Invoke-AccelCleanEmptyLocals.ps1 ####################

function Invoke-AccelCleanEmptyLocals {
    <#
      .SYNOPSIS
      Removes empty Terraform locals blocks (`locals {}`) from platform_* directories.
      Works even if block spans multiple lines or has only whitespace inside.
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Path
    )

    $root = Resolve-AccelPath -Path $Path
    $targets = @('platform_connectivity','platform_management') | ForEach-Object { Join-Path $root $_ }

    $rxEmptyLocals = [regex]::new(
        'locals\s*\{\s*\}',
        [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
    )

    $rxMultiline = [regex]::new(
        'locals\s*\{(?:\s|\r|\n)*\}',
        [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
    )

    $changed = 0

    foreach ($dir in $targets) {
        if (-not (Test-Path -LiteralPath $dir -PathType Container)) { continue }

        $files = Get-ChildItem -Path $dir -Recurse -Filter '*.tf' -File -ErrorAction SilentlyContinue
        foreach ($f in $files) {
            try {
                $content = Get-Content -LiteralPath $f.FullName -Raw -ErrorAction Stop
                $newContent = $content

                # Replace both compact and multiline empty locals
                $newContent = $rxEmptyLocals.Replace($newContent, '')
                $newContent = $rxMultiline.Replace($newContent, '')

                if ($newContent -ne $content) {
                    if ($PSCmdlet.ShouldProcess($f.FullName, 'Remove empty locals blocks')) {
                        Set-Content -LiteralPath $f.FullName -Value $newContent -Encoding UTF8 -ErrorAction Stop
                        $changed++
                    }
                }
            } catch {
                Write-Warning "Failed to clean empty locals in '$($f.FullName)': $($_.Exception.Message)"
            }
        }
    }

    return $changed
}

#################### Split-ALZ-Accelerator/private/Resolve-AccelPath.ps1 ####################

function Resolve-AccelPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Path
    )
    $resolved = Resolve-Path -LiteralPath $Path -ErrorAction Stop
    if (-not (Test-Path -LiteralPath $resolved -PathType Container)) {
        throw "Path '$Path' exists but is not a directory."
    }
    return $resolved.ProviderPath
}


#################### Split-ALZ-Accelerator/private/New-AccelSymlink.ps1 ####################

function New-AccelSymlink {
    <#
      .SYNOPSIS
      Create/ensure a symbolic link. If a file/link exists, overwrite only with -Force.
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$LinkPath,
        [Parameter(Mandatory)][string]$TargetPath,
        [switch]$Force
    )

    $dir = Split-Path -Parent -Path $LinkPath
    if ($dir) { New-AccelDirectory -Path $dir -Confirm:$false | Out-Null }

    if (Test-Path -LiteralPath $LinkPath) {
        if ($Force) {
            if ($PSCmdlet.ShouldProcess($LinkPath, "Replace existing with symlink → $TargetPath")) {
                Remove-Item -LiteralPath $LinkPath -Force -ErrorAction SilentlyContinue
            }
        }
        else {
            Write-Verbose "Link already exists, skipping: $LinkPath"
            return $true
        }
    }

    if ($PSCmdlet.ShouldProcess($LinkPath, "Create symlink → $TargetPath")) {
        New-Item -ItemType SymbolicLink -Path $LinkPath -Target $TargetPath | Out-Null
    }
    return $true
}


#################### Split-ALZ-Accelerator/private/Trim-AccelConnectivityLocals.ps1 ####################

function Trim-AccelConnectivityLocals {
    <#
      .SYNOPSIS
      In platform_connectivity/locals.tf, remove assignments like
      'management_group_settings = merge(...)' and
      'management_resource_settings = merge(...)' (entire assignment, robust to formatting).
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Path
    )

    $root = Resolve-AccelPath -Path $Path
    $file = Join-Path (Join-Path $root 'platform_connectivity') 'locals.tf'
    if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { return 0 }

    try {
        $text = Get-Content -LiteralPath $file -Raw -ErrorAction Stop

        # targets we want to remove if they appear as "<name> = merge("
        $targets = @('management_group_settings','management_resource_settings')
        $removed = 0

        foreach ($name in $targets) {
            # regex to find start of "<name> = merge("
            $pat = [System.Text.RegularExpressions.Regex]::new(
                [Regex]::Escape($name) + '\s*=\s*merge\s*\(',
                [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
            )
            $matches = $pat.Matches($text)
            if ($matches.Count -eq 0) { continue }

            # remove from end to start so indices don’t shift
            for ($mi = $matches.Count - 1; $mi -ge 0; $mi--) {
                $m = $matches[$mi]
                $startIdx = $m.Index

                # find the matching closing ')' for this merge(
                $parenStart = $text.IndexOf('(', $m.Index)
                if ($parenStart -lt 0) { continue }

                $i = $parenStart + 1
                $depth = 1
                while ($i -lt $text.Length -and $depth -gt 0) {
                    $ch = $text[$i]
                    if ($ch -eq '(') { $depth++ }
                    elseif ($ch -eq ')') { $depth-- }
                    $i++
                }
                if ($depth -ne 0) { continue } # unbalanced, skip safely

                $endIdx = $i # just after the closing ')'

                # swallow any trailing commas/whitespace/newlines after the merge(...)
                while ($endIdx -lt $text.Length) {
                    $c = [char]$text[$endIdx]
                    if ($c -eq ',' -or $c -eq ' ' -or $c -eq "`t" -or $c -eq "`r" -or $c -eq "`n") { $endIdx++ }
                    else { break }
                }

                # expand start to beginning of its line
                $lineStart = $startIdx
                while ($lineStart -gt 0) {
                    $prev = [char]$text[$lineStart - 1]
                    if ($prev -eq "`n" -or $prev -eq "`r") { break }
                    $lineStart--
                }

                # remove the slice
                $text = $text.Remove($lineStart, $endIdx - $lineStart)
                $removed++
            }
        }

        if ($removed -gt 0) {
            if ($PSCmdlet.ShouldProcess($file, "Trim $removed merge assignment(s) in locals.tf")) {
                Set-Content -LiteralPath $file -Value $text -Encoding UTF8 -ErrorAction Stop
            }
        }

        return $removed
    }
    catch {
        Write-Warning "Trimming connectivity locals failed for '$file': $($_.Exception.Message)"
        return 0
    }
}


#################### Split-ALZ-Accelerator/private/New-AccelDirectory.ps1 ####################

function New-AccelDirectory {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Path
    )
    if (Test-Path -LiteralPath $Path -PathType Container) { return $true }
    if ($PSCmdlet.ShouldProcess($Path, 'Create directory')) {
        New-Item -ItemType Directory -Path $Path -Force | Out-Null
    }
    return $true
}


#################### Split-ALZ-Accelerator/private/Invoke-AccelOperation.ps1 ####################

function Invoke-AccelOperation {
    <#
      .SYNOPSIS
      Executes a single file system operation (Move/Copy/Delete/DeleteDir).
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][ValidateSet('Move','Copy','Delete','DeleteDir')]
        [string]$Action,

        [Parameter()][string]$Source, # required for Move/Copy/Delete
        [Parameter()][string]$Destination, # required for Move/Copy
        [switch]$Force
    )

    switch ($Action) {
        'Move' {
           if (-not (Test-Path -LiteralPath $Source)) { Write-Verbose "Skip move (missing): $Source"; return 'Skipped' }
           $destDir = Split-Path -Parent -Path $Destination
           if ($destDir) { New-AccelDirectory -Path $destDir | Out-Null }
           if ($PSCmdlet.ShouldProcess("$Source", "Move to $Destination")) {
               if (Test-Path -LiteralPath $Destination) {
                   if ($Force) { Remove-Item -LiteralPath $Destination -Recurse -Force -ErrorAction Stop }
                   else { Write-Warning "Destination exists, skipping: $Destination"; return 'Skipped' }
               }
               Move-Item -LiteralPath $Source -Destination $Destination -Force -ErrorAction Stop
               return 'Moved'
           }
       }

       'Copy' {
           if (-not (Test-Path -LiteralPath $Source)) { Write-Verbose "Skip copy (missing): $Source"; return 'Skipped' }
           $destDir = Split-Path -Parent -Path $Destination
           if ($destDir) { New-AccelDirectory -Path $destDir | Out-Null }
           if ($PSCmdlet.ShouldProcess("$Source", "Copy to $Destination")) {
               if (Test-Path -LiteralPath $Destination) {
                   if ($Force) { Remove-Item -LiteralPath $Destination -Recurse -Force -ErrorAction Stop }
                   else { Write-Verbose "Destination exists; leaving as-is: $Destination"; return 'Skipped' }
               }
               Copy-Item -LiteralPath $Source -Destination $Destination -Recurse -Force -ErrorAction Stop
               return 'Copied'
           }
       }

       'Delete' {
           if (-not (Test-Path -LiteralPath $Source)) { Write-Verbose "Skip delete (missing): $Source"; return 'Skipped' }
           if ($PSCmdlet.ShouldProcess("$Source", 'Delete file')) {
               Remove-Item -LiteralPath $Source -Force -ErrorAction Stop
               return 'Deleted'
           }
       }

       'DeleteDir' {
           if (-not (Test-Path -LiteralPath $Source)) { Write-Verbose "Skip delete dir (missing): $Source"; return 'Skipped' }
           if ($PSCmdlet.ShouldProcess("$Source", 'Delete directory')) {
               Remove-Item -LiteralPath $Source -Recurse -Force -ErrorAction Stop
               return 'Deleted'
           }
       }
    }
}


#################### Split-ALZ-Accelerator/private/Update-AccelModuleSources.ps1 ####################

function Update-AccelModuleSources {
    <#
      .SYNOPSIS
      Rewrite Terraform module sources from ./modules or ../modules to the correct
      relative path to _modules-accelerator, without stray quotes or escapes.
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Root,
        [string]$ModulesNewName = '_modules-accelerator'
    )

    $rootPath = Resolve-AccelPath -Path $Root
    $modulesNew = Join-Path $rootPath $ModulesNewName

    # all .tf files except under _modules-accelerator or _modules-custom
    $tfFiles = Get-ChildItem -Path $rootPath -Recurse -Filter '*.tf' -File -ErrorAction SilentlyContinue |
        Where-Object {
            $_.FullName -notlike (Join-Path $modulesNew '*') -and
            $_.FullName -notlike (Join-Path $rootPath '_modules-custom*')
        }

    $changed = 0
    foreach ($file in $tfFiles) {
        try {
            $content = Get-Content -LiteralPath $file.FullName -Raw -ErrorAction Stop

            # compute correct relative path to _modules-accelerator from this file's folder
            $rel = [System.IO.Path]::GetRelativePath($file.Directory.FullName, $modulesNew).Replace('\','/')
            if (-not $rel.EndsWith('/')) { $rel = "$rel/" }

            $newContent = $content

            # 1) Replace ONLY the prefix inside the quotes; keep the trailing module name
            # e.g. source = "./modules/config-templating" -> source = "../_modules-accelerator/config-templating"
            $patternPrefix = '(?<=\bsource\s*=\s*")\s*(?:\./|\.\./)modules/?'
            $newContent = [regex]::Replace($newContent, $patternPrefix, $rel)

            # 2) Fix any previously broken double-quote splits, e.g.
            # source = "../_modules-accelerator/"config-templating"
            $relEsc = [regex]::Escape($rel)
            $patternFixQuotes = '(?<=\bsource\s*=\s*")(' + $relEsc + ')"\s*([^"]+)"'
            $newContent = [regex]::Replace($newContent, $patternFixQuotes, '$1$2"')

            # 3) Clean up accidental backslash-escaped segments from earlier runs, e.g. \.\./_modules-accelerator/
            $newContent = $newContent -replace '(?<=\bsource\s*=\s*")\\\.\./','../'
            # Remove stray backslashes just before our prefix or ../ (conservative lookahead)
            $newContent = $newContent -replace '(?<=\bsource\s*=\s*")\\(?=(\.\./|[A-Za-z0-9_\-]+/))',''

            if ($newContent -ne $content) {
                if ($PSCmdlet.ShouldProcess($file.FullName, "Rewrite module sources → $rel")) {
                    Set-Content -LiteralPath $file.FullName -Value $newContent -Encoding UTF8 -ErrorAction Stop
                    $changed++
                }
            }
        }
        catch {
            Write-Warning "Module source rewrite failed for '$($file.FullName)': $($_.Exception.Message)"
            continue
        }
    }

    return $changed
}


#################### Split-ALZ-Accelerator/private/Split-AccelLandingZoneAutoTfvars.ps1 ####################

function Split-AccelLandingZoneAutoTfvars {
    <#
      .SYNOPSIS
      Split platform-landing-zone.auto.tfvars into:
        - root/platform.shared.auto.tfvars (custom_replacements + enable_telemetry)
        - platform_management/management.auto.tfvars (tags + management_* settings)
        - platform_connectivity/connectivity.auto.tfvars (tags + connectivity_* settings)
      then delete platform-landing-zone.auto.tfvars.

      .NOTES
      - Idempotent: if all target files already exist, it does nothing (unless -Force).
      - Conservative parsing using brace counting; assumes ALZ's basic layout.
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Path,
        [switch]$Force
    )

    $root = Resolve-AccelPath -Path $Path

    $tfvarsRoot = Join-Path $root 'platform-landing-zone.auto.tfvars'
    if (-not (Test-Path -LiteralPath $tfvarsRoot -PathType Leaf)) {
        Write-Verbose "No platform-landing-zone.auto.tfvars found at $tfvarsRoot; skipping split."
        return
    }

    $pmDir = Join-Path $root 'platform_management'
    $pcDir = Join-Path $root 'platform_connectivity'

    $mgmtFile = Join-Path $pmDir 'management.auto.tfvars'
    $connFile = Join-Path $pcDir 'connectivity.auto.tfvars'
    $sharedFile = Join-Path $root 'platform.shared.auto.tfvars'

    # If we've already split and not forcing, bail out
    if ((Test-Path -LiteralPath $mgmtFile) -and
        (Test-Path -LiteralPath $connFile) -and
        (Test-Path -LiteralPath $sharedFile) -and
        -not $Force) {
        Write-Verbose "management/ connectivity / shared .auto.tfvars already exist; skipping tfvars split."
        return
    }

    if (-not (Test-Path -LiteralPath $pmDir -PathType Container) -or
        -not (Test-Path -LiteralPath $pcDir -PathType Container)) {
        Write-Verbose "platform_* directories not found; skipping tfvars split."
        return
    }

    $text = Get-Content -LiteralPath $tfvarsRoot -Raw -ErrorAction Stop
    $nl = ($text -match "`r`n") ? "`r`n" : "`n"

    # Small helpers for parsing from the original text
    function Get-HclObjectBlock {
        param(
            [string]$Name
        )
        $idx = $text.IndexOf($Name)
        if ($idx -lt 0) { return $null }

        # find '=' then '{'
        $eqIdx = $text.IndexOf('=', $idx)
        if ($eqIdx -lt 0) { return $null }
        $openIdx = $text.IndexOf('{', $eqIdx)
        if ($openIdx -lt 0) { return $null }

        $i = $openIdx
        $depth = 0
        $closeIdx = -1
        while ($i -lt $text.Length) {
            $ch = $text[$i]
            if ($ch -eq '{') { $depth++ }
            elseif ($ch -eq '}') {
                $depth--
                if ($depth -eq 0) {
                    $closeIdx = $i
                    break
                }
            }
            $i++
        }
        if ($closeIdx -lt 0) { return $null }

        # include comments directly above, if any
        $start = $idx
        # go back to line start
        while ($start -gt 0 -and $text[$start - 1] -notin "`r","`n") { $start-- }

        # pull in a preceding /* ... */ comment if it ends immediately above
        $commentStart = $text.LastIndexOf("/*", $start - 1)
        $commentEnd = if ($commentStart -ge 0) { $text.IndexOf("*/", $commentStart) } else { -1 }
        if ($commentStart -ge 0 -and $commentEnd -ge 0 -and $commentEnd -lt $start) {
            # Only pull it in if there’s no blank line between comment and block
            $between = $text.Substring($commentEnd + 2, $start - ($commentEnd + 2))
            if ($between.Trim() -eq '') { $start = $commentStart }
        }

        $end = $closeIdx + 1
        # eat trailing whitespace
        while ($end -lt $text.Length -and $text[$end] -in " ","`t","`r","`n") { $end++ }

        [pscustomobject]@{
            Name = $Name
            Start = $start
            End = $end
            Text = $text.Substring($start, $end - $start)
        }
    }

    function Get-SimpleAssignmentLine {
        param(
            [string]$Name
        )
        $idx = $text.IndexOf($Name)
        if ($idx -lt 0) { return $null }

        # go to start-of-line
        $start = $idx
        while ($start -gt 0 -and $text[$start-1] -notin "`r","`n") { $start-- }

        # go to end-of-line
        $end = $idx
        while ($end -lt $text.Length -and $text[$end] -notin "`r","`n") { $end++ }
        if ($end -lt $text.Length) { $end++ } # include newline

        [pscustomobject]@{
            Name = $Name
            Start = $start
            End = $end
            Text = $text.Substring($start, $end - $start)
        }
    }

    # Extract the pieces we care about
    $tagsBlock = Get-HclObjectBlock -Name 'tags'
    $mgmtResBlock = Get-HclObjectBlock -Name 'management_resource_settings'
    $mgmtGroupBlock = Get-HclObjectBlock -Name 'management_group_settings'
    $connTypeLine = Get-SimpleAssignmentLine -Name 'connectivity_type'
    $connRgsBlock = Get-HclObjectBlock -Name 'connectivity_resource_groups'
    $virtualHubsBlock = Get-HclObjectBlock -Name 'virtual_hubs'

    $customReplBlock = Get-HclObjectBlock -Name 'custom_replacements'
    $enableTelemetryLine = Get-SimpleAssignmentLine -Name 'enable_telemetry'

    # Build management.auto.tfvars
    $mgmtParts = @()
    if ($tagsBlock) { $mgmtParts += $tagsBlock.Text.TrimEnd() }
    if ($mgmtResBlock) { $mgmtParts += $mgmtResBlock.Text.TrimEnd() }
    if ($mgmtGroupBlock) { $mgmtParts += $mgmtGroupBlock.Text.TrimEnd() }

    $mgmtContent = if ($mgmtParts.Count -gt 0) {
        ($mgmtParts -join (@($nl,$nl))) + $nl
    } else {
        ''
    }

    # Build connectivity.auto.tfvars
    $connParts = @()
    if ($tagsBlock) { $connParts += $tagsBlock.Text.TrimEnd() }
    if ($connTypeLine) { $connParts += $connTypeLine.Text.TrimEnd() }
    if ($connRgsBlock) { $connParts += $connRgsBlock.Text.TrimEnd() }
    if ($virtualHubsBlock) { $connParts += $virtualHubsBlock.Text.TrimEnd() }

    $connContent = if ($connParts.Count -gt 0) {
        ($connParts -join (@($nl,$nl))) + $nl
    } else {
        ''
    }

    # Build platform.shared.auto.tfvars (shared bits only)
    $sharedParts = @()
    if ($customReplBlock) { $sharedParts += $customReplBlock.Text.TrimEnd() }
    if ($enableTelemetryLine) { $sharedParts += $enableTelemetryLine.Text.TrimEnd() }

    $sharedContent = if ($sharedParts.Count -gt 0) {
        ($sharedParts -join (@($nl,$nl))) + $nl
    } else {
        ''
    }

    # Write files
    if ($PSCmdlet.ShouldProcess($mgmtFile, "Write management.auto.tfvars")) {
        New-AccelDirectory -Path $pmDir -Confirm:$false | Out-Null
        Set-Content -LiteralPath $mgmtFile -Value $mgmtContent -Encoding UTF8 -ErrorAction Stop
    }

    if ($PSCmdlet.ShouldProcess($connFile, "Write connectivity.auto.tfvars")) {
        New-AccelDirectory -Path $pcDir -Confirm:$false | Out-Null
        Set-Content -LiteralPath $connFile -Value $connContent -Encoding UTF8 -ErrorAction Stop
    }

    if ($PSCmdlet.ShouldProcess($sharedFile, "Write platform.shared.auto.tfvars")) {
        Set-Content -LiteralPath $sharedFile -Value $sharedContent -Encoding UTF8 -ErrorAction Stop
    }

    # Finally, delete the original source file
    if ($PSCmdlet.ShouldProcess($tfvarsRoot, 'Delete original platform-landing-zone.auto.tfvars')) {
        Remove-Item -LiteralPath $tfvarsRoot -Force -ErrorAction Stop
    }
}

#################### Split-ALZ-Accelerator/private/Remove-AccelLines.ps1 ####################

function Remove-AccelLines {
    <#
      .SYNOPSIS
      Remove entire lines from all *.tf files under a directory if they match any given regex patterns.
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Directory,
        [Parameter(Mandatory)][string[]]$RegexPatterns
    )

    $dirPath = Resolve-AccelPath -Path $Directory
    if (-not (Test-Path -LiteralPath $dirPath -PathType Container)) { return 0 }

    $files = Get-ChildItem -Path $dirPath -Recurse -Filter '*.tf' -File -ErrorAction SilentlyContinue
    if (-not $files) { return 0 }

    # combine to single case-insensitive regex
    $rx = [regex]::new('(' + ($RegexPatterns -join '|') + ')', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)

    $changedCount = 0
    foreach ($f in $files) {
        try {
            $raw = Get-Content -LiteralPath $f.FullName -Raw -ErrorAction Stop
            $eol = ($raw -match "`r`n") ? "`r`n" : "`n"
            $lines = $raw -split "`r?`n"

            $modified = $false
            $kept = foreach ($ln in $lines) {
                if ($rx.IsMatch($ln)) { $modified = $true; continue }
                $ln
            }

            if ($modified) {
                $new = [string]::Join($eol, $kept)
                if ($PSCmdlet.ShouldProcess($f.FullName, "Remove lines matching patterns")) {
                    Set-Content -LiteralPath $f.FullName -Value $new -Encoding UTF8 -ErrorAction Stop
                    $changedCount++
                }
            }
        }
        catch {
            Write-Warning "Line cleanup failed for '$($f.FullName)': $($_.Exception.Message)"
            continue
        }
    }
    return $changedCount
}


#################### Split-ALZ-Accelerator/private/Invoke-AccelMoveFiles.ps1 ####################

function Invoke-AccelMoveFiles {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
    param(
        [Parameter(Mandatory)][string]$Path,
        [switch]$Force
    )

    $root = Resolve-AccelPath -Path $Path
    $pc = Join-Path $root 'platform_connectivity'
    $pm = Join-Path $root 'platform_management'

    if (-not $PSCmdlet.ShouldProcess($root, "Move/copy/delete files into platform_*")) { return }

    New-AccelDirectory -Path $pc -Confirm:$false | Out-Null
    New-AccelDirectory -Path $pm -Confirm:$false | Out-Null

    $ops = @()
    $ops += @{ Action='Move'; Source=(Join-Path $root 'lib'); Destination=(Join-Path $pm 'lib') }
    $ops += @{ Action='Move'; Source=(Join-Path $root 'main.connectivity.hub.and.spoke.virtual.network.tf'); Destination=(Join-Path $pc 'main.connectivity.hub.and.spoke.virtual.network.tf') }
    $ops += @{ Action='Move'; Source=(Join-Path $root 'main.connectivity.virtual.wan.tf'); Destination=(Join-Path $pc 'main.connectivity.virtual.wan.tf') }
    $ops += @{ Action='Move'; Source=(Join-Path $root 'main.management.tf'); Destination=(Join-Path $pm 'main.management.tf') }
    $ops += @{ Action='Move'; Source=(Join-Path $root 'main.resource.groups.tf'); Destination=(Join-Path $pc 'main.resource.groups.tf') }
    $ops += @{ Action='Move'; Source=(Join-Path $root 'outputs.tf'); Destination=(Join-Path $pc 'outputs.tf') }

    Get-ChildItem -Path (Join-Path $root 'variables.connectivity*') -File -ErrorAction SilentlyContinue | ForEach-Object {
        $ops += @{ Action='Move'; Source=$_.FullName; Destination=(Join-Path $pc $_.Name) }
    }
    $ops += @{ Action='Move'; Source=(Join-Path $root 'variables.management.tf'); Destination=(Join-Path $pm 'variables.management.tf') }

    $ops += @{ Action='Copy'; Source=(Join-Path $root 'terraform.tfvars.json'); Destination=(Join-Path $pc 'terraform.tfvars.json') }
    $ops += @{ Action='Copy'; Source=(Join-Path $root 'terraform.tfvars.json'); Destination=(Join-Path $pm 'terraform.tfvars.json') }
    $ops += @{ Action='Copy'; Source=(Join-Path $root 'terraform.tf'); Destination=(Join-Path $pc 'terraform.tf') }
    $ops += @{ Action='Copy'; Source=(Join-Path $root 'terraform.tf'); Destination=(Join-Path $pm 'terraform.tf') }
    $ops += @{ Action='Copy'; Source=(Join-Path $root 'main.config.tf'); Destination=(Join-Path $pc 'main.config.tf') }
    $ops += @{ Action='Copy'; Source=(Join-Path $root 'main.config.tf'); Destination=(Join-Path $pm 'main.config.tf') }
    $ops += @{ Action='Copy'; Source=(Join-Path $root 'locals.tf'); Destination=(Join-Path $pc 'locals.tf') }
    $ops += @{ Action='Copy'; Source=(Join-Path $root 'locals.tf'); Destination=(Join-Path $pm 'locals.tf') }
    $ops += @{ Action='Copy'; Source=(Join-Path $root 'variables.tf'); Destination=(Join-Path $pc 'variables.tf') }
    $ops += @{ Action='Copy'; Source=(Join-Path $root 'variables.tf'); Destination=(Join-Path $pm 'variables.tf') }

    $ops += @{ Action='Delete'; Source=(Join-Path $root 'README.md') }
    $ops += @{ Action='Delete'; Source=(Join-Path $root 'terraform.tfvars.json') }
    $ops += @{ Action='Delete'; Source=(Join-Path $root 'terraform.tf') }
    $ops += @{ Action='Delete'; Source=(Join-Path $root 'locals.tf') }
    $ops += @{ Action='Delete'; Source=(Join-Path $root 'variables.tf') }
    $ops += @{ Action='Delete'; Source=(Join-Path $root 'main.config.tf') }
    $ops += @{ Action='DeleteDir'; Source=(Join-Path $root 'scripts') }

    $summary = [ordered]@{Moved=0; Copied=0; Deleted=0; Skipped=0}

    foreach ($op in $ops) {
        try {
            $result = Invoke-AccelOperation @op -Force:$Force -Confirm:$false
        } catch {
            Write-Warning ("Failed {0} '{1}' -> '{2}': {3}" -f $op.Action,$op.Source,$op.Destination,$_.Exception.Message)
            $result = 'Skipped'
        }
        switch ($result) {
            'Moved' { $summary.Moved++ }
            'Copied' { $summary.Copied++ }
            'Deleted' { $summary.Deleted++ }
            default { $summary.Skipped++ }
        }
    }

    if ($WhatIfPreference) {
        return [pscustomobject]@{
            MovedPlanned = ($ops | Where-Object Action -eq 'Move').Count
            CopiedPlanned = ($ops | Where-Object Action -eq 'Copy').Count
            DeletedPlanned = ($ops | Where-Object { $_.Action -in 'Delete','DeleteDir' }).Count
            TotalPlanned = $ops.Count
        }
    }

    [pscustomobject]$summary
}


#################### Split-ALZ-Accelerator/private/Simplify-AccelStarterLocations.ps1 ####################

function Simplify-AccelStarterLocations {
    <#
      .SYNOPSIS
      Replace the entire variable "starter_locations" block in platform_management/variables.tf
      with the simplified version (removing the connectivity-related validation).
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param([Parameter(Mandatory)][string]$Path)

    $root = Resolve-AccelPath -Path $Path
    $file = Join-Path (Join-Path $root 'platform_management') 'variables.tf'
    if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { return $false }

    try {
        $content = Get-Content -LiteralPath $file -Raw -ErrorAction Stop
        $match = [regex]::Match($content, 'variable\s+"starter_locations"\s*\{', 'IgnoreCase')
        if (-not $match.Success) { return $false }

        $i = $match.Index + $match.Length
        $depth = 1
        while ($i -lt $content.Length) {
            $ch = $content[$i]
            if ($ch -eq '{') { $depth++ }
            elseif ($ch -eq '}') {
                $depth--
                if ($depth -eq 0) { break }
            }
            $i++
        }
        if ($depth -ne 0) {
            Write-Warning "Could not parse balanced braces for starter_locations in $file"
            return $false
        }

        $before = $content.Substring(0, $match.Index)
        $after = $content.Substring($i + 1)

        $nl = ($content -match "`r`n") ? "`r`n" : "`n"
        $replacement = @"
variable "starter_locations" {
  type = list(string)
  description = "The default for Azure resources. (e.g 'uksouth')"
  validation {
    condition = length(var.starter_locations) > 0
    error_message = "You must provide at least one starter location region."
  }
}
"@ -replace "`r?`n", $nl

        if ($PSCmdlet.ShouldProcess($file, 'Replace starter_locations validation block')) {
            Set-Content -LiteralPath $file -Value ($before + $replacement + $after) -Encoding UTF8 -ErrorAction Stop
            return $true
        }
    }
    catch {
        Write-Warning "Simplify starter_locations failed for '$file': $($_.Exception.Message)"
    }
    return $false
}