# ==============================================================================
# Tool AD Wrapper Functions
# These functions abstract AD access so the app works identically whether
# in simulation mode (in-memory DB) or live mode (ActiveDirectory module).
# 
# Usage: Replace Get-ADUser with Get-ToolADUser throughout the codebase.
#        The function checks $script:SimulationMode to decide data source.
# ==============================================================================

function Get-ToolJsonScalar {
    param(
        [string]$Json,
        [string]$PropertyName
    )

    $pattern = '"' + [regex]::Escape($PropertyName) + '"\s*:\s*(?<value>null|true|false|-?\d+(?:\.\d+)?|"(?<string>(?:\\.|[^"])*)")'
    $match = [regex]::Match($Json, $pattern)
    if (-not $match.Success) { return $null }

    $value = $match.Groups['value'].Value
    if ($value -eq 'null') { return $null }
    if ($value -eq 'true') { return $true }
    if ($value -eq 'false') { return $false }
    if ($match.Groups['string'].Success) {
        return ($match.Groups['string'].Value -replace '\\"', '"')
    }
    return $value
}

function Get-ToolJsonStringArray {
    param(
        [string]$Json,
        [string]$PropertyName
    )

    $pattern = '"' + [regex]::Escape($PropertyName) + '"\s*:\s*\[(?<items>.*?)\]'
    $match = [regex]::Match($Json, $pattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
    if (-not $match.Success) { return @() }

    return @(
        [regex]::Matches($match.Groups['items'].Value, '"(?<item>(?:\\.|[^"])*)"') |
            ForEach-Object { $_.Groups['item'].Value -replace '\\"', '"' }
    )
}

function ConvertFrom-ToolADJsonObject {
    param([string]$Json)

    $member = @(Get-ToolJsonStringArray -Json $Json -PropertyName 'member')
    $members = @(Get-ToolJsonStringArray -Json $Json -PropertyName 'Members')
    $memberOf = @(Get-ToolJsonStringArray -Json $Json -PropertyName 'MemberOf')

    [pscustomobject]@{
        GivenName         = Get-ToolJsonScalar -Json $Json -PropertyName 'GivenName'
        Surname           = Get-ToolJsonScalar -Json $Json -PropertyName 'Surname'
        Initials          = Get-ToolJsonScalar -Json $Json -PropertyName 'Initials'
        middleName        = Get-ToolJsonScalar -Json $Json -PropertyName 'middleName'
        UserPrincipalName = Get-ToolJsonScalar -Json $Json -PropertyName 'UserPrincipalName'
        Enabled           = Get-ToolJsonScalar -Json $Json -PropertyName 'Enabled'
        SamAccountName    = Get-ToolJsonScalar -Json $Json -PropertyName 'SamAccountName'
        DistinguishedName = Get-ToolJsonScalar -Json $Json -PropertyName 'DistinguishedName'
        Name              = Get-ToolJsonScalar -Json $Json -PropertyName 'Name'
        ObjectClass       = Get-ToolJsonScalar -Json $Json -PropertyName 'ObjectClass'
        DisplayName       = Get-ToolJsonScalar -Json $Json -PropertyName 'DisplayName'
        Description       = Get-ToolJsonScalar -Json $Json -PropertyName 'Description'
        EmailAddress      = Get-ToolJsonScalar -Json $Json -PropertyName 'EmailAddress'
        mail              = Get-ToolJsonScalar -Json $Json -PropertyName 'mail'
        Office            = Get-ToolJsonScalar -Json $Json -PropertyName 'Office'
        OfficePhone       = Get-ToolJsonScalar -Json $Json -PropertyName 'OfficePhone'
        mobile            = Get-ToolJsonScalar -Json $Json -PropertyName 'mobile'
        MobilePhone       = Get-ToolJsonScalar -Json $Json -PropertyName 'MobilePhone'
        telephoneNumber   = Get-ToolJsonScalar -Json $Json -PropertyName 'telephoneNumber'
        Organization      = Get-ToolJsonScalar -Json $Json -PropertyName 'Organization'
        Department        = Get-ToolJsonScalar -Json $Json -PropertyName 'Department'
        Manager           = Get-ToolJsonScalar -Json $Json -PropertyName 'Manager'
        roomNumber        = Get-ToolJsonScalar -Json $Json -PropertyName 'roomNumber'
        StreetAddress     = Get-ToolJsonScalar -Json $Json -PropertyName 'StreetAddress'
        City              = Get-ToolJsonScalar -Json $Json -PropertyName 'City'
        l                 = Get-ToolJsonScalar -Json $Json -PropertyName 'l'
        State             = Get-ToolJsonScalar -Json $Json -PropertyName 'State'
        PostalCode        = Get-ToolJsonScalar -Json $Json -PropertyName 'PostalCode'
        personalTitle     = Get-ToolJsonScalar -Json $Json -PropertyName 'personalTitle'
        payGrade          = Get-ToolJsonScalar -Json $Json -PropertyName 'payGrade'
        Title             = Get-ToolJsonScalar -Json $Json -PropertyName 'Title'
        EmployeeID        = Get-ToolJsonScalar -Json $Json -PropertyName 'EmployeeID'
        employeeType      = Get-ToolJsonScalar -Json $Json -PropertyName 'employeeType'
        gigID             = Get-ToolJsonScalar -Json $Json -PropertyName 'gigID'
        iaTrainingDate    = Get-ToolJsonScalar -Json $Json -PropertyName 'iaTrainingDate'
        LastLogonDate     = Get-ToolJsonScalar -Json $Json -PropertyName 'LastLogonDate'
        lastLogonTimestamp = Get-ToolJsonScalar -Json $Json -PropertyName 'lastLogonTimestamp'
        LockedOut         = Get-ToolJsonScalar -Json $Json -PropertyName 'LockedOut'
        HomeDirectory     = Get-ToolJsonScalar -Json $Json -PropertyName 'HomeDirectory'
        HomeDrive         = Get-ToolJsonScalar -Json $Json -PropertyName 'HomeDrive'
        MemberOf          = $memberOf
        member            = $member
        Members           = $members
    }
}

function Find-ToolADJsonObject {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$FilePath,

        [Parameter(Mandatory=$true)]
        [string]$Identity,

        [string[]]$MatchProperties = @('SamAccountName', 'Name', 'DistinguishedName')
    )

    if (-not (Test-Path $FilePath)) { return $null }

    $escapedIdentity = [regex]::Escape($Identity)
    $escapedProps = ($MatchProperties | ForEach-Object { [regex]::Escape($_) }) -join '|'
    $matchPattern = '"(?:' + $escapedProps + ')"\s*:\s*"' + $escapedIdentity + '"'

    $reader = [System.IO.StreamReader]::new($FilePath)
    try {
        $buffer = New-Object System.Text.StringBuilder
        $inObject = $false
        $depth = 0

        while (-not $reader.EndOfStream) {
            $line = $reader.ReadLine()
            $trimmed = $line.Trim()

            if (-not $inObject) {
                if ($trimmed -eq '{') {
                    $inObject = $true
                    $depth = 1
                    [void]$buffer.Clear()
                    [void]$buffer.AppendLine($line)
                }
                continue
            }

            [void]$buffer.AppendLine($line)
            $open  = ([regex]::Matches($line, '\{')).Count
            $close = ([regex]::Matches($line, '\}')).Count
            $depth += $open - $close

            if ($depth -le 0) {
                $json = $buffer.ToString() -replace ',\s*$', ''
                if ($json -imatch $matchPattern) {
                    return (ConvertFrom-ToolADJsonObject -Json $json)
                }

                $inObject = $false
                $depth = 0
                [void]$buffer.Clear()
            }
        }
    } finally {
        $reader.Dispose()
    }

    return $null
}

function Find-ToolADJsonObjectByPrefix {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$FilePath,

        [Parameter(Mandatory=$true)]
        [string]$PropertyName,

        [Parameter(Mandatory=$true)]
        [string]$Prefix
    )

    if (-not (Test-Path $FilePath)) { return $null }

    $matchPattern = '"' + [regex]::Escape($PropertyName) + '"\s*:\s*"' + [regex]::Escape($Prefix)

    $reader = [System.IO.StreamReader]::new($FilePath)
    try {
        $buffer = New-Object System.Text.StringBuilder
        $inObject = $false
        $depth = 0

        while (-not $reader.EndOfStream) {
            $line = $reader.ReadLine()
            $trimmed = $line.Trim()

            if (-not $inObject) {
                if ($trimmed -eq '{') {
                    $inObject = $true
                    $depth = 1
                    [void]$buffer.Clear()
                    [void]$buffer.AppendLine($line)
                }
                continue
            }

            [void]$buffer.AppendLine($line)
            $depth += ([regex]::Matches($line, '\{')).Count - ([regex]::Matches($line, '\}')).Count

            if ($depth -le 0) {
                $json = $buffer.ToString() -replace ',\s*$', ''
                if ($json -imatch $matchPattern) {
                    return (ConvertFrom-ToolADJsonObject -Json $json)
                }

                $inObject = $false
                $depth = 0
                [void]$buffer.Clear()
            }
        }
    } finally {
        $reader.Dispose()
    }

    return $null
}

function Get-ToolADDataPath {
    param([string]$FileName)

    $root = if ($Global:AppRoot) { $Global:AppRoot } else { Split-Path -Parent (Split-Path -Parent $PSScriptRoot) }
    return Join-Path (Join-Path $root 'AD_Data') $FileName
}

function Add-ToolADLookupKey {
    param(
        [hashtable]$Lookup,
        [string]$Key,
        [object]$Value
    )

    if ([string]::IsNullOrWhiteSpace($Key)) { return }
    $Lookup[$Key.Trim().ToUpper()] = $Value
}

function ConvertTo-ToolArray {
    param($Value)

    if ($null -eq $Value) { return @() }
    if ($Value -is [System.Array]) { return @($Value) }
    if (($Value.PSObject.Properties.Name -contains 'Count') -and ($Value.PSObject.Properties.Name -notcontains 'SamAccountName')) {
        return @($Value)
    }
    return @($Value)
}

function Split-ToolArgumentList {
    param($Value)

    if ($null -eq $Value) { return @() }

    $items = @()
    $seen = @{}
    foreach ($entry in (ConvertTo-ToolArray -Value $Value)) {
        if ($null -eq $entry) { continue }
        $text = [string]$entry
        if ([string]::IsNullOrWhiteSpace($text)) { continue }

        foreach ($item in ($text -split ',')) {
            $normalized = $item.Trim().Trim("'`"")
            if ([string]::IsNullOrWhiteSpace($normalized)) { continue }

            $key = $normalized.ToUpperInvariant()
            if ($seen.ContainsKey($key)) { continue }

            $seen[$key] = $true
            $items += $normalized
        }
    }

    return @($items)
}

function Get-ToolUDriveRoots {
    $defaultRoots = @(
        "\\ptfm-stm-san3v\U_Drives\76ARS",
        "\\ptfm-stm-san3v\U_Drives\78ARS",
        "\\ptfm-stm-san3v\U_Drives\514MSG",
        "\\ptfm-stm-san3v\U_Drives\514MXG",
        "\\ptfm-stm-san3v\U_Drives\514OG",
        "\\ptfm-stm-san3v\U_Drives\514WG",
        "\\ptfm-stm-san3v\U_Drives\714 AMXS",
        "\\ptfm-stm-san3v\U_Drives\732AS"
    )

    if ($Global:AppConfig -and $Global:AppConfig.PSObject.Properties.Name -contains 'UDrive' -and $Global:AppConfig.UDrive.Roots) {
        return @(ConvertTo-ToolArray -Value $Global:AppConfig.UDrive.Roots | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
    }

    return $defaultRoots
}

function Get-ToolUDriveOrganizationGroup {
    param([string]$Organization)

    if ([string]::IsNullOrWhiteSpace($Organization)) { return $null }

    $orgKey = $Organization.Trim().ToUpperInvariant()
    if ($Global:AppConfig -and $Global:AppConfig.PSObject.Properties.Name -contains 'UDrive' -and $Global:AppConfig.UDrive.OrganizationMappings) {
        foreach ($property in $Global:AppConfig.UDrive.OrganizationMappings.PSObject.Properties) {
            if ($property.Name.Trim().ToUpperInvariant() -eq $orgKey) {
                return [string]$property.Value
            }
        }
    }

    return $Organization.Trim()
}

function Get-ToolUDriveRootForOrganization {
    param([string]$OrganizationGroup)

    if ([string]::IsNullOrWhiteSpace($OrganizationGroup)) { return $null }

    $target = $OrganizationGroup.Trim().ToUpperInvariant()
    foreach ($root in (Get-ToolUDriveRoots)) {
        $rootText = [string]$root
        if ([string]::IsNullOrWhiteSpace($rootText)) { continue }

        $leaf = Split-Path -Path $rootText -Leaf
        if ($leaf -and $leaf.Trim().ToUpperInvariant() -eq $target) {
            return $rootText
        }
    }

    return $null
}

function Read-ToolADJsonObjects {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$FilePath
    )

    if (-not (Test-Path $FilePath)) { return }

    $reader = [System.IO.StreamReader]::new($FilePath)
    try {
        $buffer = New-Object System.Text.StringBuilder
        $inObject = $false
        $depth = 0

        while (-not $reader.EndOfStream) {
            $line = $reader.ReadLine()
            $trimmed = $line.Trim()

            if (-not $inObject) {
                if ($trimmed -eq '{') {
                    $inObject = $true
                    $depth = 1
                    [void]$buffer.Clear()
                    [void]$buffer.AppendLine($line)
                }
                continue
            }

            [void]$buffer.AppendLine($line)
            $depth += ([regex]::Matches($line, '\{')).Count - ([regex]::Matches($line, '\}')).Count

            if ($depth -le 0) {
                $json = $buffer.ToString() -replace ',\s*$', ''
                ConvertFrom-ToolADJsonObject -Json $json

                $inObject = $false
                $depth = 0
                [void]$buffer.Clear()
            }
        }
    } finally {
        $reader.Dispose()
    }
}

function Initialize-ToolADSimulationData {
    [CmdletBinding()]
    param([switch]$Force)

    if ($script:SimADDataLoaded -and -not $Force) { return }

    $script:SimADUsers = @()
    $script:SimADUserLookup = @{}
    $script:SimADGroups = @()
    $script:SimADGroupLookup = @{}

    $userCacheFile = Get-ToolADDataPath -FileName 'SimADUsers.cache.json'
    $userFile = Get-ToolADDataPath -FileName 'AFUsersDB_AD.json'
    if (Test-Path $userCacheFile) {
        $script:SimADUsers = @(ConvertTo-ToolArray -Value (Get-Content -Path $userCacheFile -Raw | ConvertFrom-Json))
    } elseif (Test-Path $userFile) {
        $script:SimADUsers = @(Read-ToolADJsonObjects -FilePath $userFile)
    }

    if ($script:SimADUsers.Count -gt 0) {
        foreach ($user in $script:SimADUsers) {
            Add-ToolADLookupKey -Lookup $script:SimADUserLookup -Key $user.SamAccountName -Value $user
            Add-ToolADLookupKey -Lookup $script:SimADUserLookup -Key $user.gigID -Value $user
            Add-ToolADLookupKey -Lookup $script:SimADUserLookup -Key "$($user.EmployeeID)$($user.employeeType)" -Value $user
            Add-ToolADLookupKey -Lookup $script:SimADUserLookup -Key $user.Name -Value $user
            Add-ToolADLookupKey -Lookup $script:SimADUserLookup -Key $user.DistinguishedName -Value $user
            Add-ToolADLookupKey -Lookup $script:SimADUserLookup -Key $user.UserPrincipalName -Value $user
            Add-ToolADLookupKey -Lookup $script:SimADUserLookup -Key $user.mail -Value $user
            Add-ToolADLookupKey -Lookup $script:SimADUserLookup -Key $user.EmailAddress -Value $user
            Add-ToolADLookupKey -Lookup $script:SimADUserLookup -Key $user.DisplayName -Value $user
        }
    }

    $groupCacheFile = Get-ToolADDataPath -FileName 'SimADGroups.cache.json'
    $groupFile = Get-ToolADDataPath -FileName 'AFGroupsDB_AD.json'
    if (Test-Path $groupCacheFile) {
        $script:SimADGroups = @(ConvertTo-ToolArray -Value (Get-Content -Path $groupCacheFile -Raw | ConvertFrom-Json))
    } elseif (Test-Path $groupFile) {
        $script:SimADGroups = @(Read-ToolADJsonObjects -FilePath $groupFile)
    }

    if ($script:SimADGroups.Count -gt 0) {
        foreach ($group in $script:SimADGroups) {
            Add-ToolADLookupKey -Lookup $script:SimADGroupLookup -Key $group.SamAccountName -Value $group
            Add-ToolADLookupKey -Lookup $script:SimADGroupLookup -Key $group.Name -Value $group
            Add-ToolADLookupKey -Lookup $script:SimADGroupLookup -Key $group.DistinguishedName -Value $group
        }
    }

    $script:SimADDataLoaded = $true
}

function Search-ToolADUser {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Query,

        [int]$MaxResults = 8
    )

    if ([string]::IsNullOrWhiteSpace($Query)) { return @() }

    if ($script:SimulationMode) {
        Initialize-ToolADSimulationData

        $trimmed = $Query.Trim()
        $upper = $trimmed.ToUpper()

        if ($trimmed -match '^\d') {
            $digits = ($trimmed -replace '[^0-9]', '')
            $pivChar = if ($trimmed -match '\d([A-Za-z])$') { $Matches[1].ToUpper() } else { '' }

            return @(
                $script:SimADUsers | Where-Object {
                    $employeeId = [string]$_.EmployeeID
                    $employeeType = ([string]$_.employeeType).ToUpper()
                    if ($pivChar) {
                        $employeeId.StartsWith($digits) -and $employeeType -eq $pivChar
                    } else {
                        $employeeId.StartsWith($digits)
                    }
                } | Select-Object -First $MaxResults
            )
        }

        $parts = $upper -split ',', 2
        $lastName = $parts[0].Trim()
        $firstPart = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' }

        return @(
            $script:SimADUsers | Where-Object {
                $surname = ([string]$_.Surname).ToUpper()
                $givenName = ([string]$_.GivenName).ToUpper()
                $name = ([string]$_.Name).ToUpper()
                $displayName = ([string]$_.DisplayName).ToUpper()

                if ($firstPart) {
                    $firstName = ($firstPart -split '\s+', 2)[0]
                    ($surname -like "$lastName*") -and ($givenName -like "$firstName*")
                } else {
                    ($surname -like "$lastName*") -or
                    ($name -like "$lastName*") -or
                    ($displayName -like "$lastName*")
                }
            } | Select-Object -First $MaxResults
        )
    }

    $filterValue = $Query.Replace("'", "''")
    if ($Query -match '^\d') {
        $digits = ($Query -replace '[^0-9]', '')
        Get-ADUser -Filter "EmployeeID -like '$digits*' -or SamAccountName -like '$filterValue*'" -Properties EmployeeID, employeeType, mail, EmailAddress, DisplayName, GivenName, Surname, Initials, middleName, MemberOf, Office, OfficePhone, mobile, MobilePhone, telephoneNumber, Organization, Department, Description, Manager, roomNumber, StreetAddress, City, l, State, PostalCode, UserPrincipalName, HomeDirectory, HomeDrive, personalTitle, payGrade, iaTrainingDate, LastLogonDate, lastLogonTimestamp, LockedOut, Enabled | Select-Object -First $MaxResults
    } else {
        Get-ADUser -Filter "Name -like '$filterValue*' -or Surname -like '$filterValue*' -or DisplayName -like '$filterValue*' -or SamAccountName -like '$filterValue*'" -Properties EmployeeID, employeeType, mail, EmailAddress, DisplayName, GivenName, Surname, Initials, middleName, MemberOf, Office, OfficePhone, mobile, MobilePhone, telephoneNumber, Organization, Department, Description, Manager, roomNumber, StreetAddress, City, l, State, PostalCode, UserPrincipalName, HomeDirectory, HomeDrive, personalTitle, payGrade, iaTrainingDate, LastLogonDate, lastLogonTimestamp, LockedOut, Enabled | Select-Object -First $MaxResults
    }
}

function Add-ToolADArrayValue {
    param(
        [object]$Object,
        [string]$PropertyName,
        [string]$Value
    )

    if ($null -eq $Object -or [string]::IsNullOrWhiteSpace($Value)) { return }

    if (-not ($Object.PSObject.Properties.Name -contains $PropertyName)) {
        $Object | Add-Member -NotePropertyName $PropertyName -NotePropertyValue @($Value)
        return
    }

    $current = @($Object.$PropertyName) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
    if ($Value -notin $current) {
        $Object.$PropertyName = @($current + $Value)
    }
}

function Remove-ToolADArrayValue {
    param(
        [object]$Object,
        [string]$PropertyName,
        [string]$Value
    )

    if ($null -eq $Object -or [string]::IsNullOrWhiteSpace($Value)) { return }
    if (-not ($Object.PSObject.Properties.Name -contains $PropertyName)) { return }

    $Object.$PropertyName = @($Object.$PropertyName | Where-Object { $_ -ne $Value })
}

function Set-ToolADUserProperty {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Identity,

        [Parameter(Mandatory=$true)]
        [string]$Property,

        [AllowNull()]
        $Value
    )

    Initialize-ToolADSimulationData
    $user = Get-ToolADUser -Identity $Identity -ErrorAction Stop
    if ($user.PSObject.Properties.Name -contains $Property) {
        $user.$Property = $Value
    } else {
        $user | Add-Member -NotePropertyName $Property -NotePropertyValue $Value
    }
    return $user
}

function Add-ToolADGroupMember {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$UserIdentity,

        [Parameter(Mandatory=$true)]
        [string]$GroupIdentity
    )

    Initialize-ToolADSimulationData
    $user = Get-ToolADUser -Identity $UserIdentity -ErrorAction Stop
    $group = Get-ToolADGroup -Identity $GroupIdentity -ErrorAction Stop

    Add-ToolADArrayValue -Object $group -PropertyName 'member' -Value $user.DistinguishedName
    Add-ToolADArrayValue -Object $group -PropertyName 'Members' -Value $user.DistinguishedName
    Add-ToolADArrayValue -Object $user -PropertyName 'MemberOf' -Value $group.DistinguishedName

    return $true
}

function Remove-ToolADGroupMember {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$UserIdentity,

        [Parameter(Mandatory=$true)]
        [string]$GroupIdentity
    )

    Initialize-ToolADSimulationData
    $user = Get-ToolADUser -Identity $UserIdentity -ErrorAction Stop
    $group = Get-ToolADGroup -Identity $GroupIdentity -ErrorAction Stop

    Remove-ToolADArrayValue -Object $group -PropertyName 'member' -Value $user.DistinguishedName
    Remove-ToolADArrayValue -Object $group -PropertyName 'Members' -Value $user.DistinguishedName
    Remove-ToolADArrayValue -Object $user -PropertyName 'MemberOf' -Value $group.DistinguishedName

    return $true
}

function Search-ToolADGroup {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Query,

        [int]$MaxResults = 10
    )

    $queryText = $Query.Trim()
    if ([string]::IsNullOrWhiteSpace($queryText)) { return }

    if ($script:SimulationMode) {
        Initialize-ToolADSimulationData
        $needle = $queryText.ToUpper()

        $matches = @(
            $script:SimADGroups | Where-Object {
                @(
                    $_.Name,
                    $_.SamAccountName,
                    $_.DisplayName,
                    $_.mail,
                    $_.EmailAddress,
                    $_.DistinguishedName
                ) | Where-Object {
                    -not [string]::IsNullOrWhiteSpace([string]$_) -and ([string]$_).ToUpper().Contains($needle)
                }
            } | Select-Object -First $MaxResults
        )

        foreach ($group in $matches) { Write-Output $group }
        return
    }

    $filterValue = $queryText.Replace("'", "''")
    Get-ADGroup -Filter "Name -like '*$filterValue*' -or SamAccountName -like '*$filterValue*' -or DisplayName -like '*$filterValue*' -or mail -like '*$filterValue*'" -Properties mail,DisplayName,SamAccountName,DistinguishedName | Select-Object -First $MaxResults
}

# ==============================================================================
# Get-ToolADUser
# Simulation: uses in-memory $script:SimADUserLookup (from AFUsersDB_AD.json)
# Live:       calls Get-ADUser
# ==============================================================================
function Get-ToolADUser {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        [string]$Identity,

        [string[]]$Properties
    )

    Process {
        if ($script:SimulationMode) {
            Write-Verbose "Get-ToolADUser: Simulation mode - lookup for '$Identity'"
            Initialize-ToolADSimulationData

            $key = $Identity.Trim().ToUpper()
            $user = $script:SimADUserLookup[$key]

            if ($user) {
                Write-Output $user
            } else {
                Write-Error "Get-ToolADUser: User '$Identity' not found."
            }
        } else {
            Write-Verbose "Get-ToolADUser: Live mode - calling Get-ADUser"
            if ($Properties) {
                Get-ADUser -Identity $Identity -Properties $Properties
            } else {
                Get-ADUser -Identity $Identity
            }
        }
    }
}

# ==============================================================================
# Get-ToolADGroup
# Simulation: searches AFGroupsDB_AD.json by SamAccountName
# Live:       calls Get-ADGroup
# ==============================================================================
function Get-ToolADGroup {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        [string]$Identity,

        [string[]]$Properties
    )

    Process {
        if ($script:SimulationMode) {
            Write-Verbose "Get-ToolADGroup: Simulation mode - searching for '$Identity'"
            Initialize-ToolADSimulationData

            $key = $Identity.Trim().ToUpper()
            $group = $script:SimADGroupLookup[$key]

            if ($group) {
                Write-Output $group
            } else {
                Write-Error "Get-ToolADGroup: Group '$Identity' not found."
            }
        } else {
            Write-Verbose "Get-ToolADGroup: Live mode - calling Get-ADGroup"
            if ($Properties) {
                Get-ADGroup -Identity $Identity -Properties $Properties
            } else {
                Get-ADGroup -Identity $Identity
            }
        }
    }
}

# ==============================================================================
# Get-ToolADGroupMember
# Simulation: reads Member array from group JSON object
# Live:       calls Get-ADGroupMember
# ==============================================================================
function Get-ToolADGroupMember {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [string]$Identity,

        [switch]$Recursive
    )

    Process {
        if ($script:SimulationMode) {
            Write-Verbose "Get-ToolADGroupMember: Simulation mode for '$Identity'"
            $group = Get-ToolADGroup -Identity $Identity
            $members = if ($group.member) { $group.member } elseif ($group.Members) { $group.Members } else { @() }

            function ConvertTo-ToolADMemberObject {
                param([string]$MemberDN)

                $memberUser = $script:SimADUserLookup[$MemberDN.Trim().ToUpper()]
                if ($memberUser) {
                    return [pscustomobject]@{
                        DistinguishedName = $memberUser.DistinguishedName
                        Name              = $memberUser.Name
                        SamAccountName    = $memberUser.SamAccountName
                        ObjectClass       = 'user'
                    }
                }

                $memberGroup = $script:SimADGroupLookup[$MemberDN.Trim().ToUpper()]
                if ($memberGroup) {
                    return [pscustomobject]@{
                        DistinguishedName = $memberGroup.DistinguishedName
                        Name              = $memberGroup.Name
                        SamAccountName    = $memberGroup.SamAccountName
                        ObjectClass       = 'group'
                    }
                }

                if ($MemberDN -match '^CN=([^,]+)') {
                    return [pscustomobject]@{
                        DistinguishedName = $MemberDN
                        Name              = $Matches[1]
                        SamAccountName    = ''
                        ObjectClass       = 'unknown'
                    }
                }

                return [pscustomobject]@{
                    DistinguishedName = $MemberDN
                    Name              = $MemberDN
                    SamAccountName    = ''
                    ObjectClass       = 'unknown'
                }
            }

            if ($group -and $members) {
                $queue = [System.Collections.Queue]::new()
                foreach ($memberDN in $members) { $queue.Enqueue([string]$memberDN) }
                $visited = @{}

                while ($queue.Count -gt 0) {
                    $memberDN = [string]$queue.Dequeue()
                    $memberKey = $memberDN.Trim().ToUpper()
                    if ($visited.ContainsKey($memberKey)) { continue }
                    $visited[$memberKey] = $true

                    $memberObject = ConvertTo-ToolADMemberObject -MemberDN $memberDN
                    Write-Output $memberObject

                    if ($Recursive -and $memberObject.ObjectClass -eq 'group') {
                        $nestedGroup = Get-ToolADGroup -Identity $memberObject.DistinguishedName -ErrorAction SilentlyContinue
                        $nestedMembers = if ($nestedGroup.member) { $nestedGroup.member } elseif ($nestedGroup.Members) { $nestedGroup.Members } else { @() }
                        foreach ($nestedMemberDN in $nestedMembers) {
                            $queue.Enqueue([string]$nestedMemberDN)
                        }
                    }
                }
            } elseif ($group) {
                Write-Verbose "Get-ToolADGroupMember: Group '$Identity' has no Member property."
            }
        } else {
            Write-Verbose "Get-ToolADGroupMember: Live mode - calling Get-ADGroupMember"
            Get-ADGroupMember -Identity $Identity -Recursive:$Recursive
        }
    }
}
