UnipharLeaversManagement.psm1

# UnipharLeaversManagement.psm1
# Functions for managing employee leavers (disabling accounts, removing access, etc.)
# Version: 1.0.1
# Last Modified: 2025-12-18

# Import UnipharSecurityAuth module functions if needed
# Note: The main script should import both modules

function Convert-MailboxToSharedWithOOO {
    <#
    .SYNOPSIS
        Converts user mailbox to shared mailbox.
 
    .DESCRIPTION
        Converts a user mailbox to a shared mailbox using Exchange Online cmdlets.
        Checks if the mailbox is already shared before attempting conversion.
 
    .PARAMETER UserPrincipalName
        UPN of the user whose mailbox should be converted.
 
    .EXAMPLE
        Convert-MailboxToSharedWithOOO -UserPrincipalName 'user@uniphar.com'
        Converts the user mailbox to a shared mailbox.
 
    .OUTPUTS
        PSCustomObject with UPN, ConversionResult, MailboxType, and Error properties.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName
    )

    $result = [PSCustomObject]@{
        UPN              = $UserPrincipalName
        ConversionResult = 'Unknown'
        MailboxType      = $null
        Error            = $null
    }

    try {
        Write-Verbose "Retrieving mailbox for $UserPrincipalName..."
        $mailbox = Get-Mailbox -Identity $UserPrincipalName -ErrorAction Stop
        $result.MailboxType = $mailbox.RecipientTypeDetails

        if ($mailbox.RecipientTypeDetails -ne 'SharedMailbox') {
            if ($PSCmdlet.ShouldProcess($UserPrincipalName, "Convert mailbox to shared")) {
                Write-Verbose "Converting mailbox to shared for $UserPrincipalName..."
                Set-Mailbox -Identity $UserPrincipalName -Type Shared -ErrorAction Stop
                $result.ConversionResult = 'ConvertedToShared'
                $result.MailboxType = 'SharedMailbox'
                Write-Verbose "Converted mailbox to shared: $UserPrincipalName"
            } else {
                $result.ConversionResult = 'WhatIf-WouldConvert'
                Write-Verbose "WhatIf: Would convert mailbox to shared: $UserPrincipalName"
            }
        } else {
            $result.ConversionResult = 'AlreadyShared'
            Write-Verbose "Mailbox already shared: $UserPrincipalName"
        }
    } catch {
        $result.Error = $_.Exception.Message
        $result.ConversionResult = "Error"
        Write-Error "Error converting mailbox for $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

function Set-OutOfOfficeMessage {
    <#
    .SYNOPSIS
        Sets automatic Out of Office reply message for a user mailbox.
 
    .DESCRIPTION
        Configures automatic replies (Out of Office) for one year using Microsoft Graph API.
        Sets both internal and external reply messages with scheduled duration.
 
    .PARAMETER UserPrincipalName
        UPN of the user whose Out of Office message should be set.
 
    .PARAMETER InternalMessage
        Message shown to internal recipients. Default is 'This user is no longer with the organization.'
 
    .PARAMETER ExternalMessage
        Message shown to external recipients. Default is 'This user is no longer with the organization.'
 
    .EXAMPLE
        Set-OutOfOfficeMessage -UserPrincipalName 'user@uniphar.com'
        Sets default leaver message for internal and external recipients for one year.
 
    .EXAMPLE
        Set-OutOfOfficeMessage -UserPrincipalName 'user@uniphar.com' -InternalMessage 'Contact hr@uniphar.com' -ExternalMessage 'This person has left the company.'
        Sets custom Out of Office messages.
 
    .OUTPUTS
        PSCustomObject with UPN, OOOSet (bool), and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName,

        [Parameter(Mandatory = $false)]
        [string]$InternalMessage = 'This user is no longer with the organization.',

        [Parameter(Mandatory = $false)]
        [string]$ExternalMessage = 'This user is no longer with the organization.'
    )

    $result = [PSCustomObject]@{
        UPN    = $UserPrincipalName
        OOOSet = $false
        Error  = $null
    }

    try {
        Write-Verbose "Setting OOO for $UserPrincipalName using Graph..."
        $oooSettings = @{
            '@odata.context'        = 'https://graph.microsoft.com/v1.0/$metadata#users(''' + $UserPrincipalName + ''')/mailboxSettings'
            automaticRepliesSetting = @{
                status                 = 'scheduled'
                externalAudience       = 'all'
                scheduledStartDateTime = @{
                    dateTime = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss')
                    timeZone = 'UTC'
                }
                scheduledEndDateTime   = @{
                    dateTime = (Get-Date).AddYears(1).ToString('yyyy-MM-ddTHH:mm:ss')
                    timeZone = 'UTC'
                }
                internalReplyMessage   = $InternalMessage
                externalReplyMessage   = $ExternalMessage
            }
        }
        Update-MgUserMailboxSetting -UserId $UserPrincipalName -BodyParameter $oooSettings -ErrorAction Stop
        $result.OOOSet = $true
        Write-Verbose "OOO set successfully for $UserPrincipalName"
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Failed to set OOO for $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

function Disable-EntraUserAccount {
    <#
    .SYNOPSIS
        Disables a user account in Entra ID (Azure AD).
 
    .DESCRIPTION
        Sets AccountEnabled property to false, preventing the user from signing in to
        Microsoft 365 and Azure services.
 
    .PARAMETER UserPrincipalName
        UPN of the user account to disable.
 
    .EXAMPLE
        Disable-EntraUserAccount -UserPrincipalName 'user@uniphar.com'
        Disables the user account in Entra ID.
 
    .OUTPUTS
        PSCustomObject with UPN, Disabled (bool), and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName
    )

    $result = [PSCustomObject]@{
        UPN      = $UserPrincipalName
        Disabled = $false
        Error    = $null
    }

    try {
        Update-MgUser -UserId $UserPrincipalName -AccountEnabled:$false -ErrorAction Stop
        $result.Disabled = $true
        Write-Verbose "Disabled Entra account: $UserPrincipalName"
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error disabling Entra account $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

function Disable-OnPremUserAccount {
    <#
    .SYNOPSIS
        Disables a user account in on-premises Active Directory.
 
    .DESCRIPTION
        Disables the AD account preventing logon to on-premises resources.
        Requires connectivity to on-premises AD via Hybrid Runbook Worker.
 
    .PARAMETER UserPrincipalName
        UPN of the user account to disable.
 
    .PARAMETER Server
        FQDN of the on-premises domain controller.
 
    .PARAMETER Credential
        PSCredential object for domain admin authentication (required for Hybrid Worker).
 
    .EXAMPLE
        $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv'
        Disable-OnPremUserAccount -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred
 
    .OUTPUTS
        PSCustomObject with UPN, Disabled (bool), and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName,

        [Parameter(Mandatory = $false)]
        [string]$Server,

        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential
    )

    $result = [PSCustomObject]@{
        UPN      = $UserPrincipalName
        Disabled = $false
        Error    = $null
    }

    try {
        $escapedUpn = Protect-LdapFilterValue $UserPrincipalName
        # Check if this looks like a samAccountName (no @ sign) or a full UPN
        $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' }
        $aduserparams = @{
            Filter      = "$filterAttribute -eq '$escapedUpn'"
            Server      = $Server
            Properties  = 'Enabled'
            ErrorAction = 'Stop'
        }
        if ($Credential) { $aduserparams['Credential'] = $Credential }

        $aduser = Get-ADUser @aduserparams

        if (-not $aduser) {
            $result.Error = 'UserNotFound'
            return $result
        }

        if ($aduser.Enabled) {
            $disparams = @{
                Identity    = $aduser.DistinguishedName
                Server      = $Server
                ErrorAction = 'Stop'
            }
            if ($Credential) { $disparams['Credential'] = $Credential }

            Disable-ADAccount @disparams
            $result.Disabled = $true
            Write-Verbose "Disabled on-prem AD account: $UserPrincipalName"
        } else {
            $result.Disabled = $false
            $result.Error = 'AlreadyDisabled'
            Write-Verbose "On-prem AD account already disabled: $UserPrincipalName"
        }
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error disabling on-prem AD account $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

# Hide user from Global Address List in Entra ID using Graph
function Hide-EntraUserFromGAL {
    <#
    .SYNOPSIS
        Hides a user from the Global Address List in Entra ID.
 
    .DESCRIPTION
        Sets ShowInAddressList property to false, hiding the user from Exchange
        Online and Outlook address lists.
 
    .PARAMETER UserPrincipalName
        UPN of the user to hide from GAL.
 
    .EXAMPLE
        Hide-EntraUserFromGAL -UserPrincipalName 'user@uniphar.com'
        Hides the user from Entra ID Global Address List.
 
    .OUTPUTS
        PSCustomObject with UPN, Hidden (bool), and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName
    )

    $result = [PSCustomObject]@{
        UPN           = $UserPrincipalName
        HiddenFromGAL = $false
        Error         = $null
    }

    try {
        Update-MgUser -UserId $UserPrincipalName -ShowInAddressList:$false -ErrorAction Stop
        $result.HiddenFromGAL = $true
        Write-Verbose "Cloud user $UserPrincipalName hidden from GAL using Graph"
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Failed to hide $UserPrincipalName from GAL: $($_.Exception.Message)"
    }

    return $result
}

# Hide user from Global Address List in on-premises AD
function Hide-OnPremUserFromGAL {
    <#
    .SYNOPSIS
        Hides a user from Global Address List in on-premises Active Directory.
 
    .DESCRIPTION
        Sets msExchHideFromAddressLists attribute to True, hiding the user from
        on-premises Exchange and Outlook address lists.
 
    .PARAMETER UserPrincipalName
        UPN of the user to hide from GAL.
 
    .PARAMETER Server
        FQDN of the on-premises domain controller.
 
    .PARAMETER Credential
        PSCredential object for domain admin authentication (required for Hybrid Worker).
 
    .EXAMPLE
        $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv'
        Hide-OnPremUserFromGAL -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred
 
    .OUTPUTS
        PSCustomObject with UPN, Hidden (bool), and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName,

        [Parameter(Mandatory = $false)]
        [string]$Server,

        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential
    )

    $result = [PSCustomObject]@{
        UPN           = $UserPrincipalName
        HiddenFromGAL = $false
        Error         = $null
    }

    try {
        $escapedUpn = Protect-LdapFilterValue $UserPrincipalName
        # Check if this looks like a samAccountName (no @ sign) or a full UPN
        if ($UserPrincipalName -notlike '*@*') {
            # This is a samAccountName
            $aduserparams = @{
                Filter      = "samAccountName -eq '$escapedUpn'"
                Server      = $Server
                Properties  = 'msExchHideFromAddressLists'
                ErrorAction = 'Stop'
            }
        } else {
            # This is a UPN
            $aduserparams = @{
                Filter      = "UserPrincipalName -eq '$escapedUpn'"
                Server      = $Server
                Properties  = 'msExchHideFromAddressLists'
                ErrorAction = 'Stop'
            }
        }
        if ($Credential) { $aduserparams['Credential'] = $Credential }

        $aduser = Get-ADUser @aduserparams

        if (-not $aduser) {
            $result.Error = 'UserNotFound'
            return $result
        }

        # Set msExchHideFromAddressLists to true
        $setParams = @{
            Identity    = $aduser.DistinguishedName
            Replace     = @{msExchHideFromAddressLists = $true }
            Server      = $Server
            ErrorAction = 'Stop'
        }
        if ($Credential) { $setParams['Credential'] = $Credential }

        Set-ADUser @setParams

        $result.HiddenFromGAL = $true
        Write-Verbose "Successfully hidden user from GAL in on-prem AD: $UserPrincipalName"
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error hiding user from GAL in on-prem AD $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

# Remove email forwarding - requires Exchange Online cmdlets
# Note: Microsoft Graph SDK does not provide cmdlets for managing mailbox forwarding
# Forwarding must be managed using Exchange Online cmdlets (Get-Mailbox, Set-Mailbox)
function Remove-MailboxForwarding {
    <#
    .SYNOPSIS
        Removes mailbox forwarding rules (placeholder - requires Exchange Online).
 
    .DESCRIPTION
        This function is a placeholder. Mailbox forwarding removal requires Exchange Online
        cmdlets (Get-Mailbox, Set-Mailbox) which are not available via Microsoft Graph SDK.
        Currently returns 'NotImplemented-RequiresExchangeOnline' status.
 
    .PARAMETER UserPrincipalName
        UPN of the user whose mailbox forwarding should be removed.
 
    .EXAMPLE
        Remove-MailboxForwarding -UserPrincipalName 'user@uniphar.com'
        Returns not implemented status.
 
    .NOTES
        Requires Exchange Online connection. Proper implementation:
        $mailbox = Get-Mailbox -Identity $UserPrincipalName
        if ($mailbox.ForwardingAddress -or $mailbox.ForwardingSmtpAddress) {
            Set-Mailbox -Identity $UserPrincipalName -ForwardingAddress $null -ForwardingSmtpAddress $null
        }
 
    .OUTPUTS
        PSCustomObject with UPN, ForwardingRemoved status, and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName
    )

    $result = [PSCustomObject]@{
        UPN               = $UserPrincipalName
        ForwardingRemoved = $false
        ForwardingTypes   = @()
        Error             = 'Requires Exchange Online cmdlets - not yet implemented'
    }

    # PLACEHOLDER - This function requires Exchange Online connection
    # Will be implemented after Exchange Online is enabled in the runbook
    #
    # Correct implementation would be:
    # try {
    # $mailbox = Get-Mailbox -Identity $UserPrincipalName -ErrorAction Stop
    # $forwardingTypes = @()
    #
    # if ($mailbox.ForwardingAddress -or $mailbox.ForwardingSmtpAddress) {
    # if ($mailbox.ForwardingAddress) { $forwardingTypes += 'Internal' }
    # if ($mailbox.ForwardingSmtpAddress) { $forwardingTypes += 'SMTP' }
    #
    # Set-Mailbox -Identity $UserPrincipalName -ForwardingAddress $null -ForwardingSmtpAddress $null -ErrorAction Stop
    # $result.ForwardingRemoved = $true
    # $result.ForwardingTypes = $forwardingTypes
    # $result.Error = $null
    # Write-Verbose "Forwarding removed for $UserPrincipalName : $($forwardingTypes -join ', ')"
    # } else {
    # $result.ForwardingTypes = @('NoForwardingFound')
    # $result.Error = $null
    # }
    # } catch {
    # $result.Error = $_.Exception.Message
    # Write-Warning "Error removing forwarding for $UserPrincipalName : $($_.Exception.Message)"
    # }

    Write-Verbose "Mailbox forwarding removal skipped for $UserPrincipalName (requires Exchange Online cmdlets)"
    return $result
}

# Reset password for on-premises AD user
function Reset-OnPremUserPassword {
    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName,

        [Parameter(Mandatory = $false)]
        [string]$Server,

        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential
    )

    $result = [PSCustomObject]@{
        UPN           = $UserPrincipalName
        PasswordReset = $false
        Error         = $null
    }

    try {
        # Generate random 90-character password
        $newPassword = New-RandomPassword -Length 90
        $securePassword = ConvertTo-SecureString -String $newPassword -AsPlainText -Force

        # Get AD user
        $escapedUpn = Protect-LdapFilterValue $UserPrincipalName
        # Check if this looks like a samAccountName (no @ sign) or a full UPN
        $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' }
        $aduserparams = @{
            Filter      = "$filterAttribute -eq '$escapedUpn'"
            Server      = $Server
            ErrorAction = 'Stop'
        }
        if ($Credential) { $aduserparams['Credential'] = $Credential }

        $aduser = Get-ADUser @aduserparams

        if (-not $aduser) {
            $result.Error = 'UserNotFound'
            return $result
        }

        # Reset password
        $setPasswordParams = @{
            Identity    = $aduser.DistinguishedName
            NewPassword = $securePassword
            Reset       = $true
            Server      = $Server
            ErrorAction = 'Stop'
        }
        if ($Credential) { $setPasswordParams['Credential'] = $Credential }

        Set-ADAccountPassword @setPasswordParams

        $result.PasswordReset = $true
        Write-Verbose "Successfully reset password for on-prem AD user: $UserPrincipalName"
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error resetting password for on-prem AD user $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

function Reset-EntraUserPassword {
    <#
    .SYNOPSIS
        Resets Entra ID user password to secure random string.
 
    .DESCRIPTION
        Generates and sets a new secure random password (90 characters) for
        Entra ID (Azure AD) user account to prevent unauthorized cloud access.
 
    .PARAMETER UserPrincipalName
        UPN of the user whose password should be reset.
 
    .EXAMPLE
        Reset-EntraUserPassword -UserPrincipalName 'user@uniphar.com'
        Resets Entra ID password to 90-character random string.
 
    .OUTPUTS
        PSCustomObject with UPN, PasswordReset (bool), and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName
    )

    $result = [PSCustomObject]@{
        UPN           = $UserPrincipalName
        PasswordReset = $false
        Error         = $null
    }

    try {
        # Generate random 90-character password
        $newPassword = New-RandomPassword -Length 90

        # Get user
        $user = Get-MgUser -UserId $UserPrincipalName -ErrorAction Stop

        if (-not $user) {
            $result.Error = 'UserNotFound'
            return $result
        }

        # Reset password using Update-MgUser
        $passwordProfile = @{
            forceChangePasswordNextSignIn = $false
            password                      = $newPassword
        }

        Update-MgUser -UserId $user.Id -PasswordProfile $passwordProfile -ErrorAction Stop

        $result.PasswordReset = $true
        Write-Verbose "Successfully reset password for Entra ID user: $UserPrincipalName"
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error resetting password for Entra ID user $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

# Remove on-premises group memberships
function Remove-OnPremGroupMemberships {
    <#
    .SYNOPSIS
        Removes all on-premises AD group memberships except Domain Users.
 
    .DESCRIPTION
        Removes user from all AD security groups (except Domain Users) and backs up
        group list to extensionAttribute14 for audit trail.
 
    .PARAMETER UserPrincipalName
        UPN of the user whose group memberships should be removed.
 
    .PARAMETER Server
        FQDN of the on-premises domain controller.
 
    .PARAMETER Credential
        PSCredential object for domain admin authentication (required for Hybrid Worker).
 
    .EXAMPLE
        $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv'
        Remove-OnPremGroupMemberships -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred
        Removes all groups, backs up list to extensionAttribute14.
 
    .OUTPUTS
        PSCustomObject with UPN, RemovedCount, BackedUp (bool), and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName,

        [Parameter(Mandatory = $false)]
        [string]$Server,

        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential
    )

    $result = [PSCustomObject]@{
        UPN           = $UserPrincipalName
        GroupsRemoved = 0
        GroupsBackup  = $null
        Error         = $null
    }

    try {
        $escapedUpn = Protect-LdapFilterValue $UserPrincipalName
        # Check if this looks like a samAccountName (no @ sign) or a full UPN
        if ($UserPrincipalName -notlike '*@*') {
            # This is a samAccountName
            $aduserparams = @{
                Filter      = "samAccountName -eq '$escapedUpn'"
                Server      = $Server
                Properties  = 'MemberOf', 'extensionAttribute14'
                ErrorAction = 'Stop'
            }
        } else {
            # This is a UPN
            $aduserparams = @{
                Filter      = "UserPrincipalName -eq '$escapedUpn'"
                Server      = $Server
                Properties  = 'MemberOf', 'extensionAttribute14'
                ErrorAction = 'Stop'
            }
        }
        if ($Credential) { $aduserparams['Credential'] = $Credential }

        $aduser = Get-ADUser @aduserparams

        if (-not $aduser) {
            $result.Error = 'UserNotFound'
            return $result
        }

        if ($aduser.MemberOf) {
            # Get group names and backup
            $groupNames = @()
            foreach ($groupDN in $aduser.MemberOf) {
                $getGroupParams = @{
                    Identity    = $groupDN
                    Server      = $Server
                    ErrorAction = 'SilentlyContinue'
                }
                if ($Credential) { $getGroupParams['Credential'] = $Credential }

                $group = Get-ADGroup @getGroupParams
                if ($group -and $group.Name -ne 'Domain Users') {
                    $groupNames += $group.Name

                    # Remove from group
                    $removeParams = @{
                        Identity    = $groupDN
                        Members     = $aduser.DistinguishedName
                        Server      = $Server
                        Confirm     = $false
                        ErrorAction = 'SilentlyContinue'
                    }
                    if ($Credential) { $removeParams['Credential'] = $Credential }

                    Remove-ADGroupMember @removeParams
                    $result.GroupsRemoved++
                }
            }

            # Backup to extensionAttribute14
            if ($groupNames.Count -gt 0) {
                $groupsBackup = ($groupNames | Sort-Object) -join ';'
                if ($groupsBackup.Length -gt 1024) {
                    $groupsBackup = $groupsBackup.Substring(0, 1024)
                    Write-Warning "On-prem groups backup truncated for $UserPrincipalName (exceeded 1024 chars)"
                }

                $setParams = @{
                    Identity    = $aduser.DistinguishedName
                    Replace     = @{extensionAttribute14 = $groupsBackup }
                    Server      = $Server
                    ErrorAction = 'Stop'
                }
                if ($Credential) { $setParams['Credential'] = $Credential }

                Set-ADUser @setParams
                $result.GroupsBackup = $groupsBackup
            }
        }

        Write-Verbose "Removed $($result.GroupsRemoved) on-prem groups from: $UserPrincipalName"
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error removing on-prem groups for $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

# Remove Entra ID group memberships and licenses
function Remove-EntraGroupMemberships {
    <#
    .SYNOPSIS
        Removes all Entra ID group memberships and licenses.
 
    .DESCRIPTION
        Removes user from all Entra ID (Azure AD) groups and assigned licenses.
        Backs up group list to extensionAttribute15 and license list to extensionAttribute13
        for audit trail. For synced users, writes extension attributes to on-prem AD.
 
    .PARAMETER UserPrincipalName
        UPN of the user whose group memberships and licenses should be removed.
 
    .PARAMETER Server
        FQDN of the on-premises domain controller (for synced users).
 
    .PARAMETER Credential
        PSCredential object for domain admin authentication (for synced users).
 
    .EXAMPLE
        Remove-EntraGroupMemberships -UserPrincipalName 'user@uniphar.com'
        Removes all Entra ID groups and licenses, backs up to extension attributes.
 
    .EXAMPLE
        $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv'
        Remove-EntraGroupMemberships -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred
        For synced user, removes cloud groups and writes backup to on-prem AD extension attributes.
 
    .OUTPUTS
        PSCustomObject with UPN, GroupsRemoved, LicensesRemoved, BackedUp (bool), and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName,

        [Parameter(Mandatory = $false)]
        [string]$Server,

        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential
    )

    $result = [PSCustomObject]@{
        UPN             = $UserPrincipalName
        GroupsRemoved   = 0
        GroupsBackup    = $null
        LicensesRemoved = 0
        LicensesBackup  = $null
        Error           = $null
    }

    try {
        $user = Get-MgUser -UserId $UserPrincipalName -Property Id, OnPremisesSyncEnabled, MemberOf, AssignedLicenses, LicenseAssignmentStates, extensionAttribute13, extensionAttribute15 -ErrorAction Stop

        if (-not $user) {
            $result.Error = 'UserNotFound'
            return $result
        }

        # Remove groups (only cloud groups, skip synced)
        $groupNames = @()
        $memberOf = Get-MgUserMemberOf -UserId $user.Id -All -ErrorAction SilentlyContinue

        if ($memberOf) {
            foreach ($membership in $memberOf) {
                if ($membership.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group') {
                    $group = Get-MgGroup -GroupId $membership.Id -Property Id, DisplayName, OnPremisesSyncEnabled -ErrorAction SilentlyContinue

                    # Only remove from cloud-only groups
                    if ($group -and -not $group.OnPremisesSyncEnabled) {
                        try {
                            Remove-MgGroupMemberByRef -GroupId $group.Id -DirectoryObjectId $user.Id -ErrorAction Stop
                            $groupNames += $group.DisplayName
                            $result.GroupsRemoved++
                        } catch {
                            Write-Verbose "Could not remove from group $($group.DisplayName): $($_.Exception.Message)"
                        }
                    }
                }
            }

            # Backup to extensionAttribute15
            if ($groupNames.Count -gt 0) {
                $groupsBackup = ($groupNames | Sort-Object) -join ';'
                if ($groupsBackup.Length -gt 1024) {
                    $groupsBackup = $groupsBackup.Substring(0, 1024)
                    Write-Warning "Entra groups backup truncated for $UserPrincipalName (exceeded 1024 chars)"
                }

                # Check if user is synced from on-prem - if so, write to on-prem AD
                if ($user.OnPremisesSyncEnabled) {
                    Write-Verbose "User is synced from on-prem, attempting to write Entra group backup to on-prem AD extensionAttribute15"
                    
                    # Validate we have on-prem connectivity parameters
                    if (-not $Server) {
                        Write-Warning "Cannot write Entra group backup to on-prem AD: Server parameter not provided"
                        $result.Error = if ($result.Error) { "$($result.Error); NoServerForBackup" } else { "NoServerForBackup" }
                    } elseif (-not $Credential) {
                        Write-Warning "Cannot write Entra group backup to on-prem AD: Credential parameter not provided"
                        $result.Error = if ($result.Error) { "$($result.Error); NoCredentialForBackup" } else { "NoCredentialForBackup" }
                    } else {
                        try {
                            $escapedUpn = Protect-LdapFilterValue $UserPrincipalName
                            $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' }
                            
                            Write-Verbose "Looking up on-prem AD user: $filterAttribute = $UserPrincipalName"
                            $aduserparams = @{
                                Filter      = "$filterAttribute -eq '$escapedUpn'"
                                Server      = $Server
                                Credential  = $Credential
                                ErrorAction = 'Stop'
                            }

                            $aduser = Get-ADUser @aduserparams

                            if ($aduser) {
                                Write-Verbose "Found on-prem AD user: $($aduser.DistinguishedName)"
                                $setParams = @{
                                    Identity    = $aduser.DistinguishedName
                                    Replace     = @{extensionAttribute15 = $groupsBackup }
                                    Server      = $Server
                                    Credential  = $Credential
                                    ErrorAction = 'Stop'
                                }

                                Set-ADUser @setParams
                                $result.GroupsBackup = $groupsBackup
                                Write-Verbose "Successfully wrote Entra groups to on-prem AD extensionAttribute15: $groupsBackup"
                            } else {
                                Write-Warning "On-prem AD user not found for $UserPrincipalName"
                                $result.Error = if ($result.Error) { "$($result.Error); ADUserNotFound" } else { "ADUserNotFound" }
                            }
                        } catch {
                            Write-Warning "Failed to write Entra group backup to on-prem AD for $UserPrincipalName : $($_.Exception.Message)"
                            $result.Error = if ($result.Error) { "$($result.Error); ADWriteError: $($_.Exception.Message)" } else { "ADWriteError: $($_.Exception.Message)" }
                        }
                    }
                } else {
                    # Cloud-only user - write directly to Entra
                    Write-Verbose "Writing Entra group backup to cloud extensionAttribute15 for cloud-only user: $UserPrincipalName"
                    try {
                        Update-MgUser -UserId $user.Id -OnPremisesExtensionAttributes @{extensionAttribute15 = $groupsBackup } -ErrorAction Stop
                        $result.GroupsBackup = $groupsBackup
                    } catch {
                        Write-Warning "Failed to write Entra group backup to cloud for $UserPrincipalName : $($_.Exception.Message)"
                    }
                }
            }
        }

        # Remove licenses (only direct assignments, not group-based)
        if ($user.AssignedLicenses -and $user.AssignedLicenses.Count -gt 0) {
            $licensesToRemove = @()
            $licenseBackup = @()
            
            # Get all organization subscriptions for license name mapping
            $allSkus = Get-MgSubscribedSku -All -ErrorAction SilentlyContinue
            $skuLookup = @{}
            foreach ($sku in $allSkus) {
                $skuLookup[$sku.SkuId] = $sku.SkuPartNumber
            }

            foreach ($license in $user.LicenseAssignmentStates) {
                # Only remove direct licenses (AssignedByGroup is null/empty)
                if ([string]::IsNullOrEmpty($license.AssignedByGroup)) {
                    $licensesToRemove += $license.SkuId
                    
                    # Store human-readable name (SkuPartNumber) if available, otherwise SkuId
                    $licenseName = if ($skuLookup.ContainsKey($license.SkuId)) {
                        $skuLookup[$license.SkuId]
                    } else {
                        $license.SkuId
                    }
                    $licenseBackup += $licenseName
                }
            }

            if ($licensesToRemove.Count -gt 0) {
                Set-MgUserLicense -UserId $user.Id -RemoveLicenses $licensesToRemove -AddLicenses @() -ErrorAction Stop
                $result.LicensesRemoved = $licensesToRemove.Count

                # Backup to extensionAttribute13 with human-readable names
                $licenseBackupString = ($licenseBackup | Sort-Object) -join ';'
                if ($licenseBackupString.Length -gt 1024) {
                    $licenseBackupString = $licenseBackupString.Substring(0, 1024)
                    Write-Warning "License backup truncated for $UserPrincipalName (exceeded 1024 chars)"
                }

                # Check if user is synced from on-prem - if so, write to on-prem AD
                if ($user.OnPremisesSyncEnabled) {
                    Write-Verbose "User is synced from on-prem, attempting to write license backup to on-prem AD extensionAttribute13"
                    
                    # Validate we have on-prem connectivity parameters
                    if (-not $Server) {
                        Write-Warning "Cannot write license backup to on-prem AD: Server parameter not provided"
                        $result.Error = if ($result.Error) { "$($result.Error); NoServerForLicenseBackup" } else { "NoServerForLicenseBackup" }
                    } elseif (-not $Credential) {
                        Write-Warning "Cannot write license backup to on-prem AD: Credential parameter not provided"
                        $result.Error = if ($result.Error) { "$($result.Error); NoCredentialForLicenseBackup" } else { "NoCredentialForLicenseBackup" }
                    } else {
                        try {
                            $escapedUpn = Protect-LdapFilterValue $UserPrincipalName
                            $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' }
                            
                            Write-Verbose "Looking up on-prem AD user for license backup: $filterAttribute = $UserPrincipalName"
                            $aduserparams = @{
                                Filter      = "$filterAttribute -eq '$escapedUpn'"
                                Server      = $Server
                                Credential  = $Credential
                                ErrorAction = 'Stop'
                            }

                            $aduser = Get-ADUser @aduserparams

                            if ($aduser) {
                                Write-Verbose "Found on-prem AD user: $($aduser.DistinguishedName)"
                                $setParams = @{
                                    Identity    = $aduser.DistinguishedName
                                    Replace     = @{extensionAttribute13 = $licenseBackupString }
                                    Server      = $Server
                                    Credential  = $Credential
                                    ErrorAction = 'Stop'
                                }

                                Set-ADUser @setParams
                                $result.LicensesBackup = $licenseBackupString
                                Write-Verbose "Successfully wrote licenses to on-prem AD extensionAttribute13: $licenseBackupString"
                            } else {
                                Write-Warning "On-prem AD user not found for license backup: $UserPrincipalName"
                                $result.Error = if ($result.Error) { "$($result.Error); ADUserNotFoundForLicense" } else { "ADUserNotFoundForLicense" }
                            }
                        } catch {
                            Write-Warning "Failed to write license backup to on-prem AD for $UserPrincipalName : $($_.Exception.Message)"
                            $result.Error = if ($result.Error) { "$($result.Error); ADLicenseWriteError: $($_.Exception.Message)" } else { "ADLicenseWriteError: $($_.Exception.Message)" }
                        }
                    }
                } else {
                    # Cloud-only user - write directly to Entra
                    Write-Verbose "Writing license backup to cloud extensionAttribute13 for cloud-only user: $UserPrincipalName"
                    try {
                        Update-MgUser -UserId $user.Id -OnPremisesExtensionAttributes @{extensionAttribute13 = $licenseBackupString } -ErrorAction Stop
                        $result.LicensesBackup = $licenseBackupString
                    } catch {
                        Write-Warning "Failed to write license backup to cloud for $UserPrincipalName : $($_.Exception.Message)"
                    }
                }
            }
        }

        Write-Verbose "Removed $($result.GroupsRemoved) Entra groups and $($result.LicensesRemoved) licenses from: $UserPrincipalName"
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error removing Entra groups/licenses for $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

# Remove authentication methods
function Remove-EntraAuthenticationMethods {
    <#
    .SYNOPSIS
        Removes all authentication methods from Entra ID user account.
 
    .DESCRIPTION
        Removes all registered authentication methods including phone numbers, email,
        FIDO2 security keys, Microsoft Authenticator app, and other MFA methods to
        prevent unauthorized access recovery.
 
    .PARAMETER UserPrincipalName
        UPN of the user whose authentication methods should be removed.
 
    .EXAMPLE
        Remove-EntraAuthenticationMethods -UserPrincipalName 'user@uniphar.com'
        Removes all MFA methods and authentication methods from the account.
 
    .OUTPUTS
        PSCustomObject with UPN, MethodsRemoved count, RemovedMethods list, and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName
    )

    $result = [PSCustomObject]@{
        UPN            = $UserPrincipalName
        MethodsRemoved = @()
        Error          = $null
    }

    try {
        $user = Get-MgUser -UserId $UserPrincipalName -Property Id -ErrorAction Stop

        if (-not $user) {
            $result.Error = 'UserNotFound'
            return $result
        }

        # Phone methods
        try {
            $phoneMethods = Get-MgUserAuthenticationPhoneMethod -UserId $user.Id -ErrorAction Stop
            foreach ($method in $phoneMethods) {
                try {
                    Remove-MgUserAuthenticationPhoneMethod -UserId $user.Id -PhoneAuthenticationMethodId $method.Id -ErrorAction Stop
                    $result.MethodsRemoved += 'Phone'
                    Write-Verbose "Removed phone method: $($method.PhoneType) - $($method.PhoneNumber)"
                } catch {
                    Write-Warning "Failed to remove phone method for $UserPrincipalName : $($_.Exception.Message)"
                }
            }
        } catch {
            if ($_.Exception.Message -notlike '*does not exist*') {
                Write-Verbose "Phone methods error: $($_.Exception.Message)"
            }
        }

        # Email methods
        try {
            $emailMethods = Get-MgUserAuthenticationEmailMethod -UserId $user.Id -ErrorAction Stop
            foreach ($method in $emailMethods) {
                try {
                    Remove-MgUserAuthenticationEmailMethod -UserId $user.Id -EmailAuthenticationMethodId $method.Id -ErrorAction Stop
                    $result.MethodsRemoved += 'Email'
                    Write-Verbose "Removed email method: $($method.EmailAddress)"
                } catch {
                    Write-Warning "Failed to remove email method for $UserPrincipalName : $($_.Exception.Message)"
                }
            }
        } catch {
            if ($_.Exception.Message -notlike '*does not exist*') {
                Write-Verbose "Email methods error: $($_.Exception.Message)"
            }
        }

        # FIDO2 (passkeys)
        try {
            $fido2Methods = Get-MgUserAuthenticationFido2Method -UserId $user.Id -ErrorAction Stop
            foreach ($method in $fido2Methods) {
                try {
                    Remove-MgUserAuthenticationFido2Method -UserId $user.Id -Fido2AuthenticationMethodId $method.Id -ErrorAction Stop
                    $result.MethodsRemoved += 'FIDO2'
                    Write-Verbose "Removed FIDO2 method: $($method.DisplayName)"
                } catch {
                    Write-Warning "Failed to remove FIDO2 method for $UserPrincipalName : $($_.Exception.Message)"
                }
            }
        } catch {
            if ($_.Exception.Message -notlike '*does not exist*') {
                Write-Verbose "FIDO2 methods error: $($_.Exception.Message)"
            }
        }

        # Microsoft Authenticator
        try {
            $msAuthMethods = Get-MgUserAuthenticationMicrosoftAuthenticatorMethod -UserId $user.Id -ErrorAction Stop
            foreach ($method in $msAuthMethods) {
                try {
                    Remove-MgUserAuthenticationMicrosoftAuthenticatorMethod -UserId $user.Id -MicrosoftAuthenticatorAuthenticationMethodId $method.Id -ErrorAction Stop
                    $result.MethodsRemoved += 'MicrosoftAuthenticator'
                    Write-Verbose "Removed Microsoft Authenticator method: $($method.DisplayName) - $($method.DeviceTag)"
                } catch {
                    Write-Warning "Failed to remove Microsoft Authenticator for $UserPrincipalName : $($_.Exception.Message)"
                    if (-not $result.Error) { $result.Error = "MicrosoftAuthenticator: $($_.Exception.Message)" }
                }
            }
        } catch {
            if ($_.Exception.Message -notlike '*does not exist*') {
                Write-Verbose "MS Authenticator methods error: $($_.Exception.Message)"
            }
        }

        # Software OATH
        try {
            $oathMethods = Get-MgUserAuthenticationSoftwareOathMethod -UserId $user.Id -ErrorAction Stop
            foreach ($method in $oathMethods) {
                try {
                    Remove-MgUserAuthenticationSoftwareOathMethod -UserId $user.Id -SoftwareOathAuthenticationMethodId $method.Id -ErrorAction Stop
                    $result.MethodsRemoved += 'SoftwareOATH'
                    Write-Verbose "Removed Software OATH method"
                } catch {
                    Write-Warning "Failed to remove Software OATH for $UserPrincipalName : $($_.Exception.Message)"
                }
            }
        } catch {
            if ($_.Exception.Message -notlike '*does not exist*') {
                Write-Verbose "Software OATH methods error: $($_.Exception.Message)"
            }
        }

        # Temporary Access Pass
        try {
            $tapMethods = Get-MgUserAuthenticationTemporaryAccessPassMethod -UserId $user.Id -ErrorAction Stop
            foreach ($method in $tapMethods) {
                try {
                    Remove-MgUserAuthenticationTemporaryAccessPassMethod -UserId $user.Id -TemporaryAccessPassAuthenticationMethodId $method.Id -ErrorAction Stop
                    $result.MethodsRemoved += 'TAP'
                    Write-Verbose "Removed Temporary Access Pass method"
                } catch {
                    Write-Warning "Failed to remove TAP for $UserPrincipalName : $($_.Exception.Message)"
                }
            }
        } catch {
            if ($_.Exception.Message -notlike '*does not exist*') {
                Write-Verbose "TAP methods error: $($_.Exception.Message)"
            }
        }

        # Windows Hello for Business
        try {
            $whfbMethods = Get-MgUserAuthenticationWindowsHelloForBusinessMethod -UserId $user.Id -ErrorAction Stop
            foreach ($method in $whfbMethods) {
                try {
                    Remove-MgUserAuthenticationWindowsHelloForBusinessMethod -UserId $user.Id -WindowsHelloForBusinessAuthenticationMethodId $method.Id -ErrorAction Stop
                    $result.MethodsRemoved += 'WindowsHello'
                    Write-Verbose "Removed Windows Hello method: $($method.DisplayName)"
                } catch {
                    Write-Warning "Failed to remove Windows Hello for $UserPrincipalName : $($_.Exception.Message)"
                }
            }
        } catch {
            if ($_.Exception.Message -notlike '*does not exist*') {
                Write-Verbose "Windows Hello methods error: $($_.Exception.Message)"
            }
        }

        # Passwordless Microsoft Authenticator
        try {
            $passwordlessMethods = Get-MgUserAuthenticationPasswordlessMicrosoftAuthenticatorMethod -UserId $user.Id -ErrorAction Stop
            foreach ($method in $passwordlessMethods) {
                try {
                    Remove-MgUserAuthenticationPasswordlessMicrosoftAuthenticatorMethod -UserId $user.Id -PasswordlessMicrosoftAuthenticatorAuthenticationMethodId $method.Id -ErrorAction Stop
                    $result.MethodsRemoved += 'Passwordless'
                    Write-Verbose "Removed Passwordless Microsoft Authenticator method: $($method.DisplayName)"
                } catch {
                    Write-Warning "Failed to remove Passwordless Authenticator for $UserPrincipalName : $($_.Exception.Message)"
                }
            }
        } catch {
            if ($_.Exception.Message -notlike '*does not exist*') {
                Write-Verbose "Passwordless methods error: $($_.Exception.Message)"
            }
        }

        Write-Verbose "Removed authentication methods from: $UserPrincipalName ($($result.MethodsRemoved.Count) total)"
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error removing authentication methods for $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

# Clear phone numbers from on-premises AD
function Clear-OnPremUserPhoneNumbers {
    <#
    .SYNOPSIS
        Clears all phone numbers from on-premises AD user account.
 
    .DESCRIPTION
        Removes telephoneNumber, mobile, facsimileTelephoneNumber, ipPhone, and otherTelephone
        attributes from on-premises Active Directory user account.
 
    .PARAMETER UserPrincipalName
        UPN of the user whose phone numbers should be cleared.
 
    .PARAMETER Server
        FQDN of the on-premises domain controller.
 
    .PARAMETER Credential
        PSCredential object for domain admin authentication (required for Hybrid Worker).
 
    .EXAMPLE
        $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv'
        Clear-OnPremUserPhoneNumbers -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred
 
    .OUTPUTS
        PSCustomObject with UPN, ClearedCount, and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName,

        [Parameter(Mandatory = $false)]
        [string]$Server,

        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential
    )

    $result = [PSCustomObject]@{
        UPN           = $UserPrincipalName
        PhonesCleared = 0
        Error         = $null
    }

    try {
        $escapedUpn = Protect-LdapFilterValue $UserPrincipalName
        # Check if this looks like a samAccountName (no @ sign) or a full UPN
        $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' }
        $aduserparams = @{
            Filter      = "$filterAttribute -eq '$escapedUpn'"
            Server      = $Server
            Properties  = 'telephoneNumber', 'mobile', 'otherTelephone', 'homePhone', 'ipPhone', 'pager'
            ErrorAction = 'Stop'
        }
        if ($Credential) { $aduserparams['Credential'] = $Credential }

        $aduser = Get-ADUser @aduserparams

        if (-not $aduser) {
            $result.Error = 'UserNotFound'
            return $result
        }

        $clearAttributes = @{}
        $phoneAttributes = @('telephoneNumber', 'mobile', 'otherTelephone', 'homePhone', 'ipPhone', 'pager')

        foreach ($attr in $phoneAttributes) {
            if ($aduser.$attr) {
                $clearAttributes[$attr] = $null
                $result.PhonesCleared++
            }
        }

        if ($clearAttributes.Count -gt 0) {
            $setParams = @{
                Identity    = $aduser.DistinguishedName
                Clear       = $clearAttributes.Keys
                Server      = $Server
                ErrorAction = 'Stop'
            }
            if ($Credential) { $setParams['Credential'] = $Credential }

            Set-ADUser @setParams
            Write-Verbose "Cleared $($result.PhonesCleared) phone attributes for: $UserPrincipalName"
        }
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error clearing on-prem phone numbers for $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

# Clear phone numbers from Entra ID
function Clear-EntraUserPhoneNumbers {
    <#
    .SYNOPSIS
        Clears all phone numbers from Entra ID user account.
 
    .DESCRIPTION
        Removes BusinessPhones, MobilePhone, and FaxNumber properties from
        Entra ID (Azure AD) user account.
 
    .PARAMETER UserPrincipalName
        UPN of the user whose phone numbers should be cleared.
 
    .EXAMPLE
        Clear-EntraUserPhoneNumbers -UserPrincipalName 'user@uniphar.com'
        Clears all phone number fields in Entra ID.
 
    .OUTPUTS
        PSCustomObject with UPN, PhonesClearedCount, and Error properties.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName
    )

    $result = [PSCustomObject]@{
        UPN           = $UserPrincipalName
        PhonesCleared = 0
        Error         = $null
    }

    try {
        $user = Get-MgUser -UserId $UserPrincipalName -Property Id, BusinessPhones, MobilePhone, FaxNumber -ErrorAction Stop

        if (-not $user) {
            $result.Error = 'UserNotFound'
            return $result
        }

        $updateParams = @{}

        if ($user.BusinessPhones -and $user.BusinessPhones.Count -gt 0) {
            $updateParams['BusinessPhones'] = @()
            $result.PhonesCleared++
        }

        if ($user.MobilePhone) {
            $updateParams['MobilePhone'] = $null
            $result.PhonesCleared++
        }

        if ($user.FaxNumber) {
            $updateParams['FaxNumber'] = $null
            $result.PhonesCleared++
        }

        if ($updateParams.Count -gt 0) {
            Update-MgUser -UserId $user.Id @updateParams -ErrorAction Stop
            Write-Verbose "Cleared $($result.PhonesCleared) phone attributes for: $UserPrincipalName"
        }
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error clearing Entra phone numbers for $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

# Remove on-premises contact object
function Remove-OnPremContact {
    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName,

        [Parameter(Mandatory = $false)]
        [string]$Server,

        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential
    )

    $result = [PSCustomObject]@{
        UPN            = $UserPrincipalName
        ContactRemoved = $false
        Error          = $null
    }

    try {
        # Search for contact by mail or display name
        $escapedUpn = Protect-LdapFilterValue $UserPrincipalName

        $searchParams = @{
            Filter      = "(mail -eq '$escapedUpn') -or (proxyAddresses -like 'smtp:$escapedUpn')"
            Server      = $Server
            ErrorAction = 'SilentlyContinue'
        }
        if ($Credential) { $searchParams['Credential'] = $Credential }

        $contact = Get-ADObject @searchParams -Properties mail, displayName | Where-Object { $_.ObjectClass -eq 'contact' }

        if ($contact) {
            $removeParams = @{
                Identity    = $contact.DistinguishedName
                Server      = $Server
                Confirm     = $false
                ErrorAction = 'Stop'
            }
            if ($Credential) { $removeParams['Credential'] = $Credential }

            Remove-ADObject @removeParams
            $result.ContactRemoved = $true
            Write-Verbose "Removed on-prem contact for: $UserPrincipalName"
        }
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error removing on-prem contact for $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

# Remove Entra ID contact object
function Remove-EntraContact {
    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName
    )

    $result = [PSCustomObject]@{
        UPN            = $UserPrincipalName
        ContactRemoved = $false
        Error          = $null
    }

    try {
        # Search for contact by mail
        $contacts = Get-MgContact -Filter "mail eq '$UserPrincipalName' or proxyAddresses/any(x:x eq 'smtp:$UserPrincipalName')" -ErrorAction SilentlyContinue

        if ($contacts) {
            foreach ($contact in $contacts) {
                Remove-MgContact -OrganizationalContactId $contact.Id -ErrorAction Stop
                $result.ContactRemoved = $true
                Write-Verbose "Removed Entra contact for: $UserPrincipalName"
            }
        }
    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error removing Entra contact for $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

# Move on-premises user to Disabled OU
function Move-OnPremUserToDisabledOU {
    <#
    .SYNOPSIS
        Moves on-premises AD user to Disabled OU and backs up original location.
 
    .DESCRIPTION
        Backs up the user's current OU to extensionAttribute12, then moves the user
        to the designated Disabled OU for terminated employees.
 
    .PARAMETER UserPrincipalName
        UPN of the user to move.
 
    .PARAMETER DisabledOU
        Distinguished Name of the Disabled OU. Default: OU=Disabled,OU=Management,OU=IT,OU=Users,OU=Uniphar,DC=uniphar,DC=local
 
    .PARAMETER Server
        FQDN of the on-premises domain controller.
 
    .PARAMETER Credential
        PSCredential object for domain admin authentication (required for Hybrid Worker).
 
    .EXAMPLE
        $cred = Get-OnPremAdCredential -KeyVaultName 'uni-core-on-prem-kv'
        Move-OnPremUserToDisabledOU -UserPrincipalName 'user@uniphar.com' -Server 'unidc10.uniphar.local' -Credential $cred
        Moves user to default Disabled OU and backs up original location.
 
    .OUTPUTS
        PSCustomObject with UPN, Moved (bool), OriginalOU, NewOU, and Error properties.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName,

        [Parameter(Mandatory = $false)]
        [string]$DisabledOU = 'OU=Disabled,OU=Management,OU=IT,OU=Users,OU=Uniphar,DC=uniphar,DC=local',

        [Parameter(Mandatory = $false)]
        [string]$Server,

        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential
    )

    $result = [PSCustomObject]@{
        UPN        = $UserPrincipalName
        Moved      = $false
        OriginalOU = $null
        NewOU      = $DisabledOU
        OUBackedUp = $false
        Error      = $null
    }

    try {
        $escapedUpn = Protect-LdapFilterValue $UserPrincipalName
        # Check if this looks like a samAccountName (no @ sign) or a full UPN
        $filterAttribute = if ($UserPrincipalName -notlike '*@*') { 'samAccountName' } else { 'UserPrincipalName' }
        
        Write-Verbose "Looking up on-prem AD user for OU move: $filterAttribute = $UserPrincipalName"
        $aduserparams = @{
            Filter      = "$filterAttribute -eq '$escapedUpn'"
            Server      = $Server
            Properties  = 'DistinguishedName', 'extensionAttribute12'
            ErrorAction = 'Stop'
        }
        if ($Credential) { $aduserparams['Credential'] = $Credential }

        $aduser = Get-ADUser @aduserparams

        if (-not $aduser) {
            $result.Error = 'UserNotFound'
            return $result
        }

        # Extract current OU from DistinguishedName
        $currentDN = $aduser.DistinguishedName
        # Remove the CN part to get just the OU path
        $currentOU = $currentDN -replace '^CN=.+?(?<!\\),', ''
        $result.OriginalOU = $currentOU

        Write-Verbose "User current location: $currentDN"
        Write-Verbose "User current OU: $currentOU"

        # Check if user is already in the target OU
        if ($currentOU -eq $DisabledOU) {
            Write-Verbose "User $UserPrincipalName is already in the Disabled OU"
            $result.Moved = $false
            $result.Error = 'AlreadyInDisabledOU'
            return $result
        }

        # Backup current OU to extensionAttribute12
        try {
            Write-Verbose "Backing up original OU to extensionAttribute12: $currentOU"
            $backupParams = @{
                Identity    = $aduser.DistinguishedName
                Replace     = @{extensionAttribute12 = $currentOU }
                Server      = $Server
                ErrorAction = 'Stop'
            }
            if ($Credential) { $backupParams['Credential'] = $Credential }

            Set-ADUser @backupParams
            $result.OUBackedUp = $true
            Write-Verbose "Successfully backed up original OU to extensionAttribute12"
        } catch {
            Write-Warning "Failed to backup OU to extensionAttribute12 for $UserPrincipalName : $($_.Exception.Message)"
            $result.Error = "OUBackupFailed: $($_.Exception.Message)"
            # Continue with move even if backup fails
        }

        # Move user to Disabled OU
        if ($PSCmdlet.ShouldProcess($UserPrincipalName, "Move to Disabled OU: $DisabledOU")) {
            Write-Verbose "Moving user to Disabled OU: $DisabledOU"
            $moveParams = @{
                Identity    = $aduser.DistinguishedName
                TargetPath  = $DisabledOU
                Server      = $Server
                ErrorAction = 'Stop'
            }
            if ($Credential) { $moveParams['Credential'] = $Credential }

            Move-ADObject @moveParams
            $result.Moved = $true
            Write-Verbose "Successfully moved $UserPrincipalName to Disabled OU"
        } else {
            Write-Verbose "WhatIf: Would move $UserPrincipalName to Disabled OU"
            $result.Moved = $false
            $result.Error = 'WhatIf'
        }

    } catch {
        $result.Error = $_.Exception.Message
        Write-Warning "Error moving user to Disabled OU for $UserPrincipalName : $($_.Exception.Message)"
    }

    return $result
}

# Export module members
Export-ModuleMember -Function @(
    'Convert-MailboxToSharedWithOOO',
    'Set-OutOfOfficeMessage',
    'Remove-MailboxForwarding',
    'Disable-EntraUserAccount',
    'Disable-OnPremUserAccount',
    'Hide-EntraUserFromGAL',
    'Hide-OnPremUserFromGAL',
    'Reset-OnPremUserPassword',
    'Reset-EntraUserPassword',
    'Remove-OnPremGroupMemberships',
    'Remove-EntraGroupMemberships',
    'Remove-EntraAuthenticationMethods',
    'Clear-OnPremUserPhoneNumbers',
    'Clear-EntraUserPhoneNumbers',
    'Remove-OnPremContact',
    'Remove-EntraContact',
    'Move-OnPremUserToDisabledOU'
)