functions/customSecurityAttributes/customSecurityAttributeAllowedValues/Export-TmfCustomSecurityAttributeAllowedValue.ps1

function Export-TmfCustomSecurityAttributeAllowedValue {
    <#
.SYNOPSIS
Exports allowed values for custom security attribute definitions.
.DESCRIPTION
Fetches customSecurityAttributeDefinitions and their allowed values; merges them into the definitions export (single source of truth). Returns objects when -OutPath is omitted. (Legacy alias: -OutPutPath)
.PARAMETER SpecificResources
Optional filter: value id or "<definitionId>_<valueId>" (comma separated accepted).
.PARAMETER OutPath
Root folder for output. (Legacy alias: -OutPutPath)
.PARAMETER ForceBeta
Use beta Graph endpoint.
.PARAMETER EmitStandalone
Also emit legacy standalone allowed values JSON file (otherwise it is removed).
.EXAMPLE
Export-TmfCustomSecurityAttributeAllowedValue -OutPath C:\config
.EXAMPLE
Export-TmfCustomSecurityAttributeAllowedValue -OutPath C:\config -EmitStandalone
#>

    [CmdletBinding()] param(
        [string[]] $SpecificResources,
        [Alias('OutPutPath')] [string] $OutPath,
        [switch] $ForceBeta,
        [switch] $EmitStandalone,
        [System.Management.Automation.PSCmdlet] $Cmdlet = $PSCmdlet
    )
    begin {
        Test-GraphConnection -Cmdlet $Cmdlet
        $resourceName = 'customSecurityAttributeAllowedValues'
        $tenant = (Invoke-MgGraphRequest -Method GET -Uri ("$($script:graphBaseUrl)/organization?`$select=displayName,id")).value
        $graph = if ($ForceBeta) {
            $script:graphBaseUrlBeta 
        } else {
            $script:graphBaseUrl1 
        }
        $export = @()
        function Convert-AllowedValue {
            param([object]$v, [string]$attributeId) [ordered]@{ displayName = ("{0}_{1}" -f $attributeId, $v.id); id = $v.id; attributeId = $attributeId; isActive = $v.isActive; present = $true } 
        }
        $definitions = @()
        try {
            $defsResp = Invoke-MgGraphRequest -Method GET -Uri "$($graph)/directory/customSecurityAttributeDefinitions?`$top=999" -ErrorAction Stop
            if ($defsResp.'@odata.nextLink') {
                do {
                    $definitions += $defsResp.value; $defsResp = Invoke-MgGraphRequest -Method GET -Uri $defsResp.'@odata.nextLink' 
                } while ($defsResp.'@odata.nextLink') 
            } else {
                $definitions += $defsResp.value 
            }
        } catch {
            Write-PSFMessage -Level Warning -FunctionName 'Export-TmfCustomSecurityAttributeAllowedValue' -Message "Unable to retrieve definitions: $($_.Exception.Message)"
        }
        $all = @()
        foreach ($def in $definitions) {
            try {
                $resp = Invoke-MgGraphRequest -Method GET -Uri ("$($graph)/directory/customSecurityAttributeDefinitions/{0}/allowedValues?`$top=999" -f [System.Web.HttpUtility]::UrlEncode($def.id))
                $vals = @()
                if ($resp.'@odata.nextLink') {
                    do {
                        $vals += $resp.value; $resp = Invoke-MgGraphRequest -Method GET -Uri $resp.'@odata.nextLink' 
                    } while ($resp.'@odata.nextLink') 
                } else {
                    $vals += $resp.value 
                }
                foreach ($val in $vals) {
                    $all += [pscustomobject]@{ definition = $def; value = $val } 
                }
            } catch {
                Write-PSFMessage -Level Warning -FunctionName 'Export-TmfCustomSecurityAttributeAllowedValue' -Message "Failed to get allowed values for definition $($def.id): $($_.Exception.Message)"
            }
        }
        Set-Variable -Name '_tmf_csattr_definitions' -Value $definitions -Scope Local
        Set-Variable -Name '_tmf_csattr_all' -Value $all -Scope Local
    }
    process {
        $all = Get-Variable -Name '_tmf_csattr_all' -Scope Local -ValueOnly
        if ($SpecificResources) {
            $filters = @(); foreach ($e in $SpecificResources) {
                $filters += $e -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } 
            }; $filters = $filters | Select-Object -Unique
            foreach ($f in $filters) {
                $match = $all | Where-Object { ("{0}_{1}" -f $_.definition.id, $_.value.id) -eq $f -or $_.value.id -eq $f }
                if ($match) {
                    foreach ($m in $match) {
                        $export += Convert-AllowedValue $m.value $m.definition.id 
                    } 
                } else {
                    Write-PSFMessage -Level Warning -FunctionName 'Export-TmfCustomSecurityAttributeAllowedValue' -String 'TMF.Export.NotFound' -StringValues $f, $resourceName, $tenant.displayName 
                }
            }
        } else {
            foreach ($entry in $all) {
                $export += Convert-AllowedValue $entry.value $entry.definition.id 
            }
        }
        Write-PSFMessage -Level Verbose -FunctionName 'Export-TmfCustomSecurityAttributeAllowedValue' -Message ("Exporting {0} allowed value(s). ForceBeta={1}" -f $export.Count, $ForceBeta)
        if (-not $OutPath) {
            return $export 
        }
    }
    end {
        if ($OutPath) {
            $root = Join-Path $OutPath 'customSecurityAttributes'
            if (-not (Test-Path $root)) {
                New-Item -Path $OutPath -Name 'customSecurityAttributes' -ItemType Directory -Force | Out-Null 
            }
            $avPath = Join-Path $root $resourceName
            $legacyFile = Join-Path $avPath "$resourceName.json"
            if ($EmitStandalone) {
                if (-not (Test-Path $avPath)) {
                    New-Item -Path $root -Name $resourceName -ItemType Directory -Force | Out-Null 
                }
                ($export | ConvertTo-Json -Depth 15) | Out-File -FilePath $legacyFile -Encoding utf8 -Force
                Write-PSFMessage -Level Verbose -FunctionName 'Export-TmfCustomSecurityAttributeAllowedValue' -Message 'Standalone allowed values file emitted (legacy compatibility).'
            } else {
                if (Test-Path $legacyFile) {
                    try {
                        Remove-Item -Path $legacyFile -Force -ErrorAction Stop; Write-PSFMessage -Level Verbose -FunctionName 'Export-TmfCustomSecurityAttributeAllowedValue' -Message 'Removed legacy standalone allowed values file to enforce single source of truth.' 
                    } catch {
                        Write-PSFMessage -Level Warning -FunctionName 'Export-TmfCustomSecurityAttributeAllowedValue' -Message "Failed to remove legacy standalone file: $($_.Exception.Message)" 
                    }
                }
            }
            $definitionsDir = Join-Path $root 'customSecurityAttributeDefinitions'
            if (-not (Test-Path $definitionsDir)) {
                New-Item -Path $root -Name 'customSecurityAttributeDefinitions' -ItemType Directory -Force | Out-Null 
            }
            $definitionsFile = Join-Path $definitionsDir 'customSecurityAttributeDefinitions.json'
            $definitionsFromFile = $null
            if (Test-Path $definitionsFile) {
                try {
                    $raw = Get-Content -Path $definitionsFile -Raw -ErrorAction Stop
                    if ($raw.Trim()) {
                        $definitionsFromFile = $raw | ConvertFrom-Json -ErrorAction Stop 
                    } else {
                        $definitionsFromFile = @() 
                    }
                } catch {
                    Write-PSFMessage -Level Warning -FunctionName 'Export-TmfCustomSecurityAttributeAllowedValue' -Message "Failed to parse existing definitions file; recreating. Error: $($_.Exception.Message)"
                    $definitionsFromFile = $null
                }
            }
            $graphDefs = Get-Variable -Name '_tmf_csattr_definitions' -Scope Local -ValueOnly
            if (-not $definitionsFromFile) {
                $definitionsFromFile = @() 
            }
            if ($definitionsFromFile -isnot [System.Collections.IEnumerable]) {
                $definitionsFromFile = @($definitionsFromFile) 
            }
            $index = @{}
            foreach ($d in $definitionsFromFile) {
                if ($d.displayName) {
                    $index[$d.displayName] = $d 
                } 
            }
            foreach ($gdef in $graphDefs) {
                $id = $gdef.id.split("_")[1]
                if (-not $index.ContainsKey($id)) {
                    $new = [pscustomobject]@{
                        displayName             = $id
                        description             = $gdef.description
                        attributeSet            = $gdef.attributeSet
                        name                    = $gdef.name
                        isCollection            = $gdef.isCollection
                        isSearchable            = $gdef.isSearchable
                        status                  = $gdef.status
                        type                    = $gdef.type
                        usePreDefinedValuesOnly = $gdef.usePreDefinedValuesOnly
                        allowedValues           = @()
                        present                 = $true
                    }
                    $definitionsFromFile += $new
                    $index[$id] = $new
                }
            }
            foreach ($def in $index.Keys) {
                $index[$def].allowedValues = @() 
            }
            foreach ($val in $export) {
                if ($index.ContainsKey($val.attributeId.split("_")[1])) {
                    $index[$val.attributeId.split("_")[1]].allowedValues += [pscustomobject]@{ displayName = $val.id; isActive = $val.isActive }
                }
            }
            foreach ($def in $index.Values) {
                $def.allowedValues = @($def.allowedValues | Sort-Object -Property displayName -Unique) 
            }
            $definitionsFromFile = @($index.Values | Sort-Object -Property displayName)
            if ($definitionsFromFile) {
                ($definitionsFromFile | ConvertTo-Json -Depth 15) | Out-File -FilePath $definitionsFile -Encoding utf8 -Force
                Write-PSFMessage -Level Verbose -FunctionName 'Export-TmfCustomSecurityAttributeAllowedValue' -Message ("Merged allowedValues into definitions file ({0} definitions)." -f $definitionsFromFile.Count)
            }
            else {
                Write-PSFMessage -Level Verbose -FunctionName 'Export-TmfCustomSecurityAttributeAllowedValue' -Message "No data in customSecurityAttributeDefinitions found."
            }
        }
    }
}