FilterBox/Write-SRxFilterBranch.ps1

<#
.SYNOPSIS
    Iteration-1 ISOLATED WORKER (skeleton) for the Filter Box.
    Writes a customization BRANCH (a hierarchical path of Terms under the layer's
    "Customizations" block) to the remote O365 Termstore, app-only, in a separate
    process so the TemplateExplorer UI never blocks.

.DESIGN
    Reuses the PROVEN branch-walk logic already in SRxCore:
        Get-SRxCustomizationWriter -> [CustomizationWriter].Start($pack)
    We only wrap it in the isolated-process + result-contract + local-sync envelope
    (the same envelope proven by the Property Box worker Write-SRxTermBatch.ps1).

    Result contract (identical shape to the Property Box worker):
      - writes JSON to -ResultPath (parent polls this)
      - ALSO emits a single __JSON__{...} line to stdout.

.PARAMETER PackPath
    Path to a temp JSON file holding the BranchPack (written by the launcher to avoid
    command-line length limits). Shape (see architecture doc §7.1):
      {
        Operation, Scope, Optype, Environment, Design, TermID,
        Branch: [ { Name, NodeName, KeyAttribute, KeyAttributeValue,
                    Attributes:{...}, Deprecated, CustomizationTarget,
                    ProvisioningActivity } , ... top..leaf ],
        BaselineModified
      }

.PARAMETER ResultPath
    File the parent polls; this script writes the result JSON there on completion.

.NOTES
    SKELETON: stages are marked TODO where the exact CustomizationWriter pack shape or
    the local-sync XML patch must be finalized. It is written to PARSE cleanly and to
    fail safe (always emits a result) so the UI reap loop works from day one.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory)] [string]$RootPath,
    [Parameter(Mandatory)] [string]$PackPath,
    [Parameter(Mandatory)] [string]$ResultPath,
    [Parameter(Mandatory=$false)] [switch]$Force,
    [Parameter(Mandatory=$false)] [ValidateSet('JSON','XML')] [string]$Format = 'JSON'
)

$ErrorActionPreference = 'Stop'

# ------------------------------------------------------------------ result plumbing
function Write-SRxFilterResult {
    param([bool]$Success, [bool]$Conflict, $Data, [string]$ErrorText)
    $payload = [pscustomobject]@{
        Success   = $Success
        Conflict  = $Conflict
        Data      = $Data
        Error     = $ErrorText
        Pid       = $PID
        Ended     = (Get-Date).ToString('o')
    }
    $json = $payload | ConvertTo-Json -Depth 25 -Compress
    try {
        $dir = Split-Path -Parent $ResultPath
        if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
        Set-Content -LiteralPath $ResultPath -Value $json -Encoding UTF8
    } catch { }
    Write-Host ("__JSON__" + $json)
}

# ------------------------------------------------------------------ helpers
function ConvertFrom-SRxJsonFile {
    param([string]$Path)
    if (-not (Test-Path $Path)) { throw "Input file not found: $Path" }
    $raw = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
    if ([string]::IsNullOrWhiteSpace($raw)) { throw "Input file empty: $Path" }
    return ($raw | ConvertFrom-Json)
}

# Convert a JSON Branch descriptor array into the [System.Collections.Stack] of
# term-node descriptors that CustomizationWriter.Start expects (leaf peeked first).
function ConvertTo-SRxNodesStack {
    param($Branch)   # array top..leaf
    $stack = [System.Collections.Stack]::new()
    # push top..leaf so that Peek() returns the LEAF (matches CustomizationWriter walk)
    foreach ($d in $Branch) {
        $attrs = @{}
        if ($d.Attributes) {
            foreach ($p in $d.Attributes.PSObject.Properties) { $attrs[$p.Name] = [string]$p.Value }
        }
        if ($d.NodeName)            { $attrs['ts_NodeName']            = [string]$d.NodeName }
        if ($d.KeyAttribute)        { $attrs['ts_KeyAttribute']        = [string]$d.KeyAttribute }
        if ($d.KeyAttributeValue)   { $attrs['ts_KeyAttributeValue']   = [string]$d.KeyAttributeValue }
        if ($d.ProvisioningActivity){ $attrs['ts_ProvisioningActivity']= [string]$d.ProvisioningActivity }
        if ($d.CustomizationTarget) { $attrs['ts_CustomizationTarget'] = $true }
        if ($d.Deprecated)          { $attrs['ts_Deprecated']          = $true }

        $termNode = [pscustomobject]@{
            Name       = [string]$d.Name
            Attributes = $attrs
            Scope      = [int]$d.Scope
            xPath      = [string]$d.XPath
            Operation  = [string]$d.Operation
            optype     = [int]$d.Optype
        }
        $stack.Push($termNode)
    }
    return ,$stack
}

try {
    # ============================================================ STAGE 1: bootstrap
    Set-Location -Path $RootPath
    $externalScript = Join-Path $RootPath 'loadmodule.ps1'
    . $externalScript
    if ($null -ne $LoadModule) { $LoadModule.Invoke("SRxCore") | Out-Null }
    Initialize-SRxEnv -LoadModule2 $LoadModule -RootPath $RootPath | Out-Null
    Connect-SRxSPOService
    if ($null -eq $global:SRxEnv) {
        if (Get-Command Start-SRxShell -ErrorAction SilentlyContinue) {
            Start-SRxShell -RootPath $RootPath -isJob | Out-Null
        } elseif (Get-Command Initialize-SRxEnv -ErrorAction SilentlyContinue) {
            Initialize-SRxEnv -RootPath $RootPath | Out-Null
        } else {
            throw "SRx environment could not be initialised from '$RootPath'."
        }
    }
    if ($null -eq $global:SRxEnv -or $null -eq $global:SRxEnv.Tenancy) {
        throw "global:SRxEnv or global:SRxEnv.Tenancy is null after bootstrap."
    }

    # Import the Filter Box module so we can call Sync-SRxLocalFilterBranch here.
    try { Import-Module (Join-Path $PSScriptRoot 'SRxFilterBox.psm1') -DisableNameChecking -Force -ErrorAction SilentlyContinue } catch { }
    try { Import-Module (Join-Path $PSScriptRoot 'SRxCustomizationJson.psm1') -DisableNameChecking -Force -ErrorAction SilentlyContinue } catch { }
    try { . (Join-Path $PSScriptRoot 'Write-SRxJsonCustomization.ps1') } catch { }
    # ============================================================ STAGE 2: read pack
    $pack = ConvertFrom-SRxJsonFile -Path $PackPath
    $operation = [string]$pack.Operation
    $scope     = [int]$pack.Scope

    # ============================================================ STAGE 3: (opt) concurrency
    # TODO (Iteration 2): if (-not $Force -and $pack.BaselineModified) { compare host
    # Customizations-block LastModifiedDate; on drift -> Write-SRxFilterResult conflict; exit 2 }

    # ============================================================ STAGE 4: WRITE BRANCH
    # Reuse the proven CSOM branch walk from SRxCore. CustomizationWriter.Start expects a
    # $nodesPack with { nodesStack; ts_Environment; ts_Design; ts_TermID } and reads
    # scope/operation/optype from the PEEK'd term node.
     # decide effective format (JSON only for term-hosted layers 1..3; scope 0 -> XML)
     $effFormat = $Format
     if ([int]$pack.Scope -le 0) { $effFormat = 'XML' }
 
     if (-not (Get-Command Get-SRxCustomizationWriter -ErrorAction SilentlyContinue)) { throw "Get-SRxCustomizationWriter not available (SRxCore not loaded)." }
     
 
     if ($effFormat -eq 'JSON') {
        if (-not (Get-Command New-SRxJsonContainerTerm -ErrorAction SilentlyContinue)) { throw "New-SRxJsonContainerTerm not available (SRxCustomizationJson not loaded)." }
        if (-not (Get-Command Write-SRxJsonCustomization -ErrorAction SilentlyContinue)) { throw "Write-SRxJsonCustomization not available (dot-source it in STAGE 1)." }

        # build the container descriptor (Name = C_<hash>, Attributes = ts_* incl. compressed json)
        $container = New-SRxJsonContainerTerm -Pack $pack -Operation $operation

        # app-only connection (same pattern CustomizationWriter uses)
        $conn = Get-SRxConnection -siteUrl $global:SRxEnv.Tenancy.AdminUrl

        $props = @{}
        foreach ($k in $container.Attributes.Keys) { $props[[string]$k] = [string]$container.Attributes[$k] }

        $jw = Write-SRxJsonCustomization -Connection $conn `
                -Scope       ([int]$pack.Scope) `
                -Optype      ([int]$pack.Optype) `
                -Environment ([string]$pack.Environment) `
                -Design      ([string]$pack.Design) `
                -TermID      ([string]$pack.TermID) `
                -ContainerName ([string]$container.Name) `
                -Operation   ([string]$operation) `
                -Props       $props
        if (-not $jw.Success) { throw "Direct JSON write failed: $($jw.Error)" }
    }
    else {
        $writer = Get-SRxCustomizationWriter
        # legacy XML-mirror branch (unchanged) - keep your existing else{} body
        $nodesStack = ConvertTo-SRxNodesStack -Branch $pack.Branch
        $writerPack = [pscustomobject]@{
            nodesStack     = ,$nodesStack
            ts_Environment = [string]$pack.Environment
            ts_Design      = [string]$pack.Design
            ts_TermID      = [string]$pack.TermID
        }
        $writer.Start($writerPack)
    }
    # ============================================================ STAGE 5: LOCAL SYNC
    # Patch the cached termSet.xml Customizations sub-tree for this branch + regen JSON.
    # (termSetFiltered.xml is deprecated/unused -> not touched.)
    $syncData    = $null
    $syncWarning = $null
    try {
        if (Get-Command Sync-SRxLocalFilterBranch -ErrorAction SilentlyContinue) {
            if ($effFormat -eq 'JSON') {
                $sync = Sync-SRxLocalJsonCustomization -Pack $pack -Operation $operation
            } else {
                $sync = Sync-SRxLocalFilterBranch -Pack $pack
            }
            if ($sync.Success) { $syncData = $sync }
            else               { $syncWarning = "Local sync failed: $($sync.Error)" }
        } else {
            $syncWarning = "Sync-SRxLocalFilterBranch not available; local cache not updated."
        }
    } catch {
        $syncWarning = "Local sync threw: $($_.Exception.Message)"
    }

    # ============================================================ STAGE 6: result
    $data = [pscustomobject]@{
        Operation   = $operation
        Scope       = $scope
        FilterNodes = if ($syncData) { $syncData.FilterNodes } else { $null }  # new _filterNodes slice
        OuterXml    = if ($syncData) { $syncData.OuterXml }    else { $null }  # patched branch xml
        SyncWarning = $syncWarning
    }
    Write-SRxFilterResult -Success $true -Conflict $false -Data $data -ErrorText $null
    exit 0
}
catch {
    $msg = $_.Exception.Message
    if ($_.Exception.InnerException) { $msg += " | inner: " + $_.Exception.InnerException.Message }
    Write-SRxFilterResult -Success $false -Conflict $false -Data $null -ErrorText $msg
    exit 1
}