
function writeHeaders
{

    ################################################################
    # --------------------------------------------------------------
    # Header für $DAWGruppenAnlegenOutFilePath anlegen
    # --------------------------------------------------------------
    "Basket.Import.ADGroup.Create" | Out-File $DAWGruppenAnlegenOutFilePath -Encoding "utf8NoBOM"
    "GUIMandator" + ";" + "GUIADGroup.Name" + ";" + "GUIADGroup.Description" + ";" + "GUIMandatorADGroupType" + ";" + "GUIADGroup.DisplayName" + ";" + "GUIProcessingNote" + ";" + "OldName" | Out-File $DAWGruppenAnlegenOutFilePath -Append -Encoding "utf8NoBom"
   
    # --------------------------------------------------------------
    # Header für $DAWGruppenverwaltungOutFilePath anlegen
    # --------------------------------------------------------------
    "Basket.Import.ADGroup.ManageMembers" | Out-File "$DAWGruppenverwaltungOutFilePath" -Encoding "utf8NoBom"
    "GUIMandator" + ";" + "GUIMandatorADGroupType" + ";" + "GUIADGroup.LDAP" + ";" + "GUIADGroup.ADGroups.Add.LDAPs" + ";" + "GUIADGroup.ADGroups.Rem.LDAPs" + ";" + "GUIADGroup.Accounts.Add.LDAPs" + ";" + "GUIADGroup.Accounts.Rem.LDAPs" + ";" + "GUIProcessingNote" + ";" + "ADObjectType" + ";" + "Quellgruppe" | Out-File $DAWGruppenverwaltungOutFilePath -Encoding "utf8NoBom" -Append 

    "Basket.Import.ADGroup.ManageMembers" | Out-File "$DAWGruppenverwaltungNeuOutFilePath" -Encoding "utf8NoBom"
    "GUIMandator" + ";" + "GUIMandatorADGroupType" + ";" + "GUIADGroup.LDAP" + ";" + "GUIADGroup.ADGroups.Add.LDAPs" + ";" + "GUIADGroup.ADGroups.Rem.LDAPs" + ";" + "GUIADGroup.Accounts.Add.LDAPs" + ";" + "GUIADGroup.Accounts.Rem.LDAPs" + ";" + "GUIProcessingNote" + ";" + "ADObjectType" + ";" + "Quellgruppe" | Out-File $DAWGruppenverwaltungNeuOutFilePath -Encoding "utf8NoBom" -Append 
   
    # --------------------------------------------------------------
    # Header für $KURSSollProfile anlegen
    # --------------------------------------------------------------
    "Profilnummer;Name;Beschreibung;Profilverantwortliche OE/Stelle;Info;InfoKurs" | Out-File $KURSSollProfileOutFilePath -Encoding "utf8NoBom"
    
    # --------------------------------------------------------------
    # Header für $IstMitarbeiterProfile anlegen
    # --------------------------------------------------------------
    "Personalnummer;;Profilnummer;Name Berechtigungsprofil" | Out-File "$KURSIstMitarbeiterProfileOutFilePath" -Encoding "utf8NoBom"
    ################################################################
}

function getObjectMemberships
{
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $adObjectEntryName,
        [Parameter(Mandatory = $true, Position = 1)]
        [string] $memberShipType,
        [Parameter(Mandatory = $true, Position = 2)]
        [string] $memberShipOriginalType
    )

    $key = getMembershipIndexKey $adObjectEntryName $memberShipType

    $bucket = $ADObjectMembershipsIndexed[$key]
    $objectMemberships = if ($null -eq $bucket)
    {
        @() 
    }
    else
    {
        @($bucket) 
    }

    $objectMemberships | Add-Member -Force "Origin" $memberShipOriginalType
    $objectMemberships | Add-Member -Force "OriginName" $adObjectEntryName
    return $objectMemberships
}

function getAllUserMemberships
{
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        $adObjectEntry
    )

    $result = [System.Collections.Generic.List[object]]::new()
    $result.AddRange(@(getObjectMemberships $adObjectEntry.ObjectName "user" "user"))

    $groupMemberships = @(getObjectMemberships $adObjectEntry.ObjectName "group" "group")
    foreach ($groupMembership in $groupMemberships)
    {
        $result.AddRange(@(getObjectMemberships $groupMembership.MemberName "user" "group"))
    }
    # it's the same, bad odd syntax
    # return ,$result.ToArray()
    return @($result)
}

function getUsersByMemberGroup
{
    <#
    .SYNOPSIS
        Retrieves the user membership entries for a given group via the
        prebuilt membership index (O(1) lookup, exact class match).
    .PARAMETER GroupName
        The group whose user members are requested (matched against GroupName).
    .OUTPUTS
        Array of matching membership entries (empty array if none).
        Object references are preserved from the index.
    #>
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $GroupName
    )

    # BUGFIX 2026-07-02: was indexing the flat array $ADObjectMemberships with a string
    # key, which fails silently (non-terminating Int32 conversion error -> $null) and
    # dropped ALL indirect (group-inherited) user memberships. Must use the hashtable.
    $bucket = $ADObjectMembershipsIndexed[(getMembershipIndexKey $GroupName "user")]
    $result = if ($null -eq $bucket)
    {
        @() 
    }
    else
    {
        @($bucket) 
    }
    return $result
}

# function getAllUserMemberships
# {
#     param([Parameter(Mandatory, Position = 0)] $adObjectEntry)

#     $result = [System.Collections.Generic.List[object]]::new()
#     $result.AddRange(@(getObjectMemberships $adObjectEntry.ObjectName "user" "user"))

#     $groupMemberships = @(getObjectMemberships $adObjectEntry.ObjectName "group" "group")
#     foreach ($groupMembership in $groupMemberships)
#     {
#         $result.AddRange(@(getObjectMemberships $groupMembership.MemberName "user" "group"))
#     }

#     # ist beides gleich
#     # return ,$result.ToArray()
#     return @($result)
# }

# function getObjectMemberships
# {
#     param
#     (
#         [Parameter(Mandatory = $true, Position = 0)]
#         [String] $adObjectEntryName,
#         [Parameter(Mandatory = $true, Position = 1)]
#         [string] $memberShipType,
#         [Parameter(Mandatory = $true, Position = 2)]
#         [string] $memberShipOriginalType
#     )   

#     $objectMemberships = @($ADObjectMemberships | Where-Object {($_.GroupName -ieq $adObjectEntryName) -and ($_.MemberClass -ieq $memberShipType)})
#     # this is not very nice - we should only add members if they don´t exist - not just to set the values
#     $objectMemberships | Add-Member -Force "Origin" $memberShipOriginalType
#     $objectMemberships | Add-Member -Force "OriginName" $adObjectEntryName           
#     return $objectMemberships
# }

function getMembershipIndexKey
{
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $GroupName,
        [Parameter(Mandatory = $true, Position = 1)]
        [string] $MemberClass
    )
    return "$GroupName`0$MemberClass".ToLowerInvariant()
}

function newObjectMembershipsIndex
{
    <#
    .SYNOPSIS
        Builds a lookup index over membership entries for O(1) retrieval by
        GroupName + MemberClass, replacing repeated linear Where-Object scans.
    .PARAMETER ADObjectMemberships
        The flat collection of membership entries to index.
    .OUTPUTS
        Hashtable mapping "<groupname>\0<memberclass>" (lowercased) to a
        List[object] of the matching entries. Object references are preserved.
    #>
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowEmptyCollection()]
        $ADObjectMemberships
    )

    $index = @{}

    foreach ($entry in $ADObjectMemberships)
    {
        # NUL separator avoids key collisions between e.g. "ab"+"c" and "a"+"bc"
        $key = getMembershipIndexKey $($entry.GroupName) $($entry.MemberClass)
        $key0 = "$($entry.GroupName)`0$($entry.MemberClass)".ToLowerInvariant()
        if (-not($key -eq $key0))
        {
            throw "this is BAAAD"
        }
        $bucket = $index[$key]
        if ($null -eq $bucket)
        {
            $bucket = [System.Collections.Generic.List[object]]::new()
            $index[$key] = $bucket
        }
        $bucket.Add($entry)
    }

    return $index
}

function getRoleType
{
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        $adObjectEntry,
        [Parameter(Mandatory = $true, Position = 1)]
        [String] $typeTag
    )
    # $FileSystemAccessEntriesNew = @()
    $roleType = "unknown"
    # BUGFIX 2026-07-02: 'Sort-Object -Property "Order"' does not resolve hashtable keys on
    # Windows PowerShell 5.1 (silent no-op, declaration order preserved). The scriptblock
    # form works identically on 5.1 and 7, making rule evaluation order engine-independent.
    $relevantRoleTypes = $SPK.GroupManagementMappings | Where-Object {$_.OU -ieq $typeTag} | Sort-Object { [int]$_.Order }
    foreach ($relevantRoleType in $relevantRoleTypes)
    {
        if ($adObjectEntry.OldName -cmatch $relevantRoleType.TagRegExp)
        {
            $roleType = $relevantRoleType.TypeName 
            break
        }
    }
    # Ausnahmebehandlungen in Absprache mit N.Zimmer am 21.01.2019
    #--------------------------------------------------------------
    if (($typeTag -ieq $FI.TypeTagSecurity) -and ($roleType -ieq "unknown"))
    {
        $roleType = "GPO"
    }
    #--------------------------------------------------------------    
    return $roletype
}

function buildPrefixMatcher
{
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$CsvPath
    )

    if (-not (Test-Path $CsvPath))
    {
        throw "Prefix CSV not found: $CsvPath" 
    }

    # length -> HashSet[string] of prefixes with exactly that length
    $byLength = [System.Collections.Generic.Dictionary[int, System.Collections.Generic.HashSet[string]]]::new()

    # foreach ($row in (Import-Csv -Path $CsvPath))
    foreach ($row in $SPK.ProfilPrefixes)
    {
        $prefix = $row.Prefix
        if ([string]::IsNullOrEmpty($prefix)) 
        {
            continue 
        }

        $len = $prefix.Length
        $bucket = $null
        if (-not $byLength.TryGetValue($len, [ref] $bucket))
        {
            $bucket = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
            $byLength[$len] = $bucket
        }
        [void]$bucket.Add($prefix)
    }

    # Pre-compute the distinct lengths once, sorted descending so the most specific
    # (longest) prefix wins first - mirrors the "specific before generic" ordering
    # of a well-formed elseif cascade.
    $lengths = $byLength.Keys | Sort-Object -Descending

    # Return a small state object carrying both structures.
    return [pscustomobject]@{
        ByLength = $byLength
        Lengths  = $lengths
    }
}

function testKnownPrefixes
{
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$Value,
        [Parameter(Mandatory = $true, Position = 1)]
        $Matcher
    )

    if ([string]::IsNullOrEmpty($Value))
    {
        return $false 
    }

    $valueLen = $Value.Length
    foreach ($len in $Matcher.Lengths)
    {
        if ($len -gt $valueLen)
        {
            continue 
        }              # prefix longer than value -> can't match
        $candidate = $Value.Substring(0, $len)
        $bucket = $Matcher.ByLength[$len]
        if ($bucket.Contains($candidate))
        {
            return $true                                  # found a known prefix
        }
    }
    return $false
}

# function getNormalizedDirectoryPaths
# {
#     $analyzedDirectoryEntries = @()
#     #  $header = "Verzeichnisname", "Endpunktname", "Gruppe Lesen", "Gruppe Aendern", "Benutzer mit Leserecht", "Benutzer mit Aenderungsrecht", "Rollen mit Leserecht", "Rollen mit Aenderungsrecht", "Verzeichnisart", "normierter Pfad"
#     # $directoriesFilePathEntries = Get-Content -Path $DirectoriesFilePath | Select-Object -Skip 1 | ConvertFrom-Csv -Delimiter ";" -Header $header
#     # $directoriesFilePathEntries = Get-Content -Path $DirectoriesInFilePath | Select-Object -Skip 1 | ConvertFrom-Csv -Delimiter ";"
#     $directoriesFilePathEntries = Get-Content -Path $DirectoriesInFilePath | ConvertFrom-Csv -Delimiter ";"
#     # $headers = ($directoriesFilePathEntries | Get-Member -MemberType NoteProperty)
#     # $interstingHeader = $headers.Name -match "Gruppe .ndern"
#     foreach ($FilePathEntry in $directoriesFilePathEntries)
#     {
#         $analyzedDirectoryEntries += $FilePathEntry.Name
#     }
# }


function postProcess
{
    param (
        [Parameter(Mandatory = $false, Position = 0)]  
        [bool]$convertToAscii = $false
    ) 
    
    #########################################################
    # just to be sure that nothing happens: comment it out until you know what`s happening here !
    #########################################################

    # $tmpFilePath = moveToInstituteTmpPath $DAWGruppenAnlegenOutFilePath $true  
    # $firstlines = Get-Content -Path $tmpFilePath -TotalCount 2
    # Get-Content -Path $tmpFilePath -TotalCount 1 | Out-File -Force $DAWGruppenAnlegenOutFilePath
    # Get-Content -Path $tmpFilePath | Select-Object -Skip 2 | ConvertFrom-Csv -Delimiter ";" | select 'GUIMandator', 'GUIADGroup.Name', 'GUIADGroup.Description', 'GUIMandatorADGroupType', 'GUIADGroup.DisplayName', 'GUIProcessingNote' | ConvertTo-Csv -Delimiter ';' -NoTypeInformation | Out-File -FilePath $DAWGruppenAnlegenOutFilePath -Encoding UTF8 -Append
    # # Import-Csv $tmpFilePath | select 'GUIMandator','GUIADGroup.Name','GUIADGroup.Description','GUIMandatorADGroupType','GUIADGroup.DisplayName','GUIProcessingNote' | Export-Csv -Path $DAWGruppenAnlegenOutFilePath -Delimiter ";" -Encoding UTF8

    # $tmpFilePath = moveToInstituteTmpPath $DAWGruppenverwaltungOutFilePath $true  
    # $firstlines = Get-Content -Path $tmpFilePath -TotalCount 2
    # Get-Content -Path $tmpFilePath -TotalCount 1 | Out-File -Force $DAWGruppenverwaltungOutFilePath
    # Get-Content -Path $tmpFilePath | Select-Object -Skip 2 | ConvertFrom-Csv -Delimiter ";" | select "GUIMandator", "GUIMandatorADGroupType", "GUIADGroup.LDAP", "GUIADGroup.ADGroups.Add.LDAPs", "GUIADGroup.ADGroups.Rem.LDAPs", "GUIADGroup.Accounts.Add.LDAPs", "GUIADGroup.Accounts.Rem.LDAPs", "GUIProcessingNote"  | ConvertTo-Csv -Delimiter ';' -NoTypeInformation | Out-File -FilePath $DAWGruppenverwaltungOutFilePath -Encoding UTF8 -Append
 
    # # Import-Csv $tmpFilePath | select "GUIMandator","GUIMandatorADGroupType","GUIADGroup.LDAP","GUIADGroup.ADGroups.Add.LDAPs","GUIADGroup.ADGroups.Rem.LDAPs","GUIADGroup.Accounts.Add.LDAPs","GUIADGroup.Accounts.Rem.LDAPs","GUIProcessingNote" | Export-Csv -Path $DAWGruppenverwaltungOutFilePath -Delimiter ";" -Encoding UTF8 -NoTypeInformation
    # $tmpFilePath = moveToInstituteTmpPath $DAWGruppenverwaltungNeuOutFilePath $true  
    # $firstlines = Get-Content -Path $tmpFilePath -TotalCount 2 
    # Get-Content -Path $tmpFilePath -TotalCount 1 | Out-File -Force $DAWGruppenverwaltungNeuOutFilePath
    # Get-Content -Path $tmpFilePath | Select-Object -Skip 2 | ConvertFrom-Csv -Delimiter ";" | select "GUIMandator", "GUIMandatorADGroupType", "GUIADGroup.LDAP", "GUIADGroup.ADGroups.Add.LDAPs", "GUIADGroup.ADGroups.Rem.LDAPs", "GUIADGroup.Accounts.Add.LDAPs", "GUIADGroup.Accounts.Rem.LDAPs", "GUIProcessingNote"  | ConvertTo-Csv -Delimiter ';' -NoTypeInformation | Out-File -FilePath $DAWGruppenverwaltungNeuOutFilePath -Encoding UTF8 -Append

    ####################################################### todo ?
    # $KURSIstProfileOutFilePath 
    # $KURSIstProfileBerechtigungenOutFilePath
    # $KURSIstStellenBerechtigungenOutFilePath 
    # $KURSIstStellenProfileOutFilePath 
    # $KURSIstOEsBerechtigungenOutFilePath 
    # $KURSIstOEsProfileOutFilePath
    # $KURSIstMitarbeiterProfileOutFilePath    
    # $KURSSollStellenfunktionenOutFilePath
    # $KURSSollProfileOutFilePath
    # $KURSSollProfileBerechtigungenOutFilePath
    # $KURSSollStellenfunktionenBerechtigungenOutFilePath
    # $KURSSollStellenfunktionenProfileOutFilePath
    ####################################################### todo ?
    
}

