FilterBox/Write-SRxJsonCustomization.ps1

<#
================================================================================
  Write-SRxJsonCustomization.ps1 (save-conflict fix)
  -----------------------------------------------------------------------------
  DIRECT single-term JSON customization writer. Host resolution mirrors
  CustomizationWriter.setProvisioningBlock, but drops -IncludeChildTerms, the
  per-term 5s sleep, and the CustomizationWriter constructor DB reload.

  SAVE-CONFLICT FIX:
    * NEW container -> Add-PnPTermToTerm -CustomProperties (ATOMIC create+props,
                        single server call => no "save conflict", no retry backoff).
    * EXISTING -> re-fetch the term fresh by Id, then SetCustomProperty +
                        Invoke-PnPQuery (fresh version avoids conflict).
    * REMOVE -> Remove-PnPTerm.

  Requires SRxCustomizationJson.psm1 (Split-SRxJsonProperty). Get-SRxHostingTerm /
  Set-SRxTermDesign come from SRxCore.
================================================================================
#>


function Write-SRxJsonCustomization {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] $Connection,
        [Parameter(Mandatory)] [int]$Scope,
        [Parameter(Mandatory=$false)] [int]$Optype = -1,
        [Parameter(Mandatory=$false)] [string]$Environment = '',
        [Parameter(Mandatory=$false)] [string]$Design = '',
        [Parameter(Mandatory=$false)] [string]$TermID = '',
        [Parameter(Mandatory)] [string]$ContainerName,
        [Parameter(Mandatory)] [string]$Operation,           # 'add' | 'remove'
        [Parameter(Mandatory=$false)] [hashtable]$Props = @{}
    )

    $result = [pscustomobject]@{ Success=$false; Error=$null; ContainerId=$null; CustomizationsId=$null }
    try {
        # ---------- 1) hosting term = the pack's TermID (we already have it) ----------
        if ([string]::IsNullOrEmpty($TermID)) { throw "TermID is required for JSON write (scope $Scope)." }
        $siteTerm = Get-PnPTerm -Identity ([GUID]$TermID) -Connection $Connection -ErrorAction SilentlyContinue
        if ($null -eq $siteTerm) { throw "Hosting term $TermID not found (scope $Scope)." }
        Get-PnPProperty -ClientObject $siteTerm -Property CustomProperties -Connection $Connection | Out-Null

        # ---------- 2) find/create the Customizations child term ----------
        $custId = $null
        try { $custId = $siteTerm.CustomProperties['ts_ProvisioningTemplate'] } catch {}
        $customizations = $null
        if ($custId) {
            $customizations = Get-PnPTerm -Identity ([GUID]$custId) -Connection $Connection -ErrorAction SilentlyContinue
        }
        if ($null -eq $customizations) {
            # not yet present -> create under the hosting term, atomically record the pointer
            $customizations = Add-PnPTermToTerm -ParentTerm $siteTerm.Id -Name 'Customizations' -Connection $Connection
            $fresh = Get-PnPTerm -Identity $siteTerm.Id -Connection $Connection
            $fresh.SetCustomProperty('ts_ProvisioningTemplate', [string]$customizations.Id)
            Invoke-PnPQuery -RetryCount 5 -Connection $Connection
        }
        if ($null -eq $customizations) { throw "Could not resolve/create Customizations host under term $TermID." }
        $result.CustomizationsId = [string]$customizations.Id
        # ---------- 3) find the C_<hash> container child (ONE shallow level) ----------
        Get-PnPProperty -ClientObject $customizations -Property Terms -Connection $Connection | Out-Null
        $container = $null
        foreach ($t in $customizations.Terms) { if ([string]$t.Name -eq $ContainerName) { $container = $t; break } }

        # ---------- 4) remove ----------
        if ($Operation -eq 'remove') {
            if ($container) { Remove-PnPTerm -Identity $container.Id -Force -Connection $Connection }
            $result.Success = $true
            return $result
        }

        # ---------- 4b) flatten + chunk props into a single hashtable ----------
        $flat = @{}
        foreach ($k in @($Props.Keys)) {
            $v = [string]$Props[$k]
            if ($k -eq 'ts_CustomizationJson') {
                $chunked = Split-SRxJsonProperty -BaseKey 'ts_CustomizationJson' -Value $v
                foreach ($ck in $chunked.Keys) { $flat[[string]$ck] = [string]$chunked[$ck] }
            } else {
                $flat[[string]$k] = $v
            }
        }

        if (-not $container) {
            # ATOMIC create WITH properties -> single server call -> no save conflict
            $container = Add-PnPTermToTerm -ParentTerm $customizations.Id -Name $ContainerName -CustomProperties $flat -Connection $Connection
            $result.ContainerId = [string]$container.Id
            $result.Success = $true
            return $result
        }
        else {
            # EXISTING: re-fetch fresh to get a clean version, then update
            $fresh = Get-PnPTerm -Identity $container.Id -Connection $Connection
            foreach ($k in $flat.Keys) { $fresh.SetCustomProperty([string]$k, [string]$flat[$k]) }
            Invoke-PnPQuery -RetryCount 5 -Connection $Connection
            $result.ContainerId = [string]$fresh.Id
            $result.Success = $true
            return $result
        }
    }
    catch {
        $result.Error = $_.Exception.Message
        return $result
    }
}