Public/Invoke-sqmRestoreDatabase.ps1

<#
.SYNOPSIS
Restores a database from a backup file, with support for single-server and AlwaysOn environments.
 
.DESCRIPTION
The function performs a controlled database restore. It automatically detects whether the target
database belongs to an AlwaysOn availability group and removes it from the AG if so (including
deletion on secondary replicas). By default, once the restore completes, the database is
automatically re-added to the AG (Add-DbaAgDatabase with SeedingMode Automatic), which also
reseeds the secondaries - use -NoRejoinAvailabilityGroup to leave it standalone instead. Database
users are exported before the restore (for later recovery). Optionally a backup of the original
database can be created. After the restore, users are recovered, orphaned users are repaired,
non-existent Windows logins are removed, and the database owner is set to the SA account
(regardless of its name).
 
AG or not, the function behaves identically apart from the AG-specific steps themselves (remove
from AG, delete on secondaries, rejoin/reseed). Every operation - the optional pre-restore backup,
user export, single-user mode, the restore itself, user re-import, and cleanup - always targets a
single "working instance" determined once at the start of the run: for an AG-managed database this
is always the AG's Primary replica (restoring or altering a database against a Secondary is not
meaningful), never whichever instance happened to be passed in; for a non-AG database it is simply
the given -SqlInstance. AG membership/topology itself is looked up via -SqlInstance as the entry
point (that view is available cluster-wide from any replica), but all actual work then happens on
the working instance.
 
The function can also restore a sequence of backups (Full + Diff + Logs) using the `-BackupFiles`
parameter, which accepts a list of backup files in the correct order (Full, then Diff, then Logs).
 
Before user export and before the restore, the configured PBM policy (DefaultPolicy) is
temporarily disabled (on the working instance) to avoid restrictions during user creation. It is
re-enabled after completion.
 
If the database is in use, it is automatically set to single-user mode after the user export
(and switched back to multi-user after the restore) - single-user is applied only after the
export, not before, since Export-DbaUser needs its own connection to the database and would
otherwise fail with "database is already open and can only have one user at a time". If the
database is already found in SINGLE_USER or RESTRICTED_USER mode when the function starts
(e.g. left over from a previous interrupted restore), it is immediately reset to MULTI_USER
(disconnecting whatever session was holding that one connection slot) before anything else runs -
otherwise every step needing its own connection, starting with Export-DbaUser, would fail the
same way.
 
AG-membership is normally auto-detected at the start of the run. If a previous run already
removed the database from the AG but failed before rejoining it (or crashed/was interrupted), a
retry would no longer auto-detect it as an AG database and would silently skip secondary cleanup
and rejoin/reseed entirely - a database restore for an AG database must never end up outside the
AG without a clear signal. Use `-AvailabilityGroupName` to force AG-aware handling regardless of
current live membership. Every rejoin attempt (success and failure) is additionally written to
the Windows Application Event Log (source "sqmAlwaysOn", same source as
Repair-sqmAlwaysOnDatabases) so a failed reseed is visible to monitoring even if the returned
result objects are never inspected.
 
Policy: every database on an AG-capable instance must end up on AlwaysOn - this applies even if
the database was NOT an AG member before the restore. If the database isn't currently in any AG
and `-AvailabilityGroupName` wasn't given, the instance's Availability Groups are checked: with
exactly one AG, the restored database is automatically added to it (with seeding); with zero AGs
there is nothing to join and it stays standalone; with more than one AG the run aborts, since it
would be ambiguous which AG should receive the database - `-AvailabilityGroupName` must be given
explicitly in that case. Use `-KeepAlwaysOn` to opt out of this auto-join for a database that
should genuinely stay standalone (e.g. a scratch/test restore), or `-NoRejoinAvailabilityGroup` to
still do the detection/logging but skip the actual join.
 
The rejoin itself runs in a `finally` block once the restore has actually completed, so it is
attempted even if a later, non-critical post-restore cleanup step (user re-import, orphan-user
repair, stale Windows-login removal, owner assignment) throws - including with -EnableException.
A database that was an AG member at the start of the run will never be left un-rejoined just
because one of those cleanup steps failed.
 
.PARAMETER SqlInstance
Entry-point SQL Server instance (e.g. "localhost", "SQL01\INSTANCE") used to discover AG
membership/topology. Default: current computer name. For an AG-managed database, the actual work
(backup, export, restore, cleanup) always runs against the AG's Primary replica instead - see
DESCRIPTION.
 
.PARAMETER SqlCredential
Alternative credentials for the target instance.
 
.PARAMETER BackupFile
Path to the full backup file (.bak). Can also be an array for striped backups.
For sequential restore (Full + Diff + Logs) use `-BackupFiles`.
 
.PARAMETER BackupFiles
Array of backup files in order: Full, then Diff (optional), then Logs (optional).
Example: @("C:\Backup\Full.bak", "C:\Backup\Diff.bak", "C:\Backup\Log1.trn", "C:\Backup\Log2.trn").
Can be used instead of `-BackupFile`.
 
.PARAMETER DatabaseName
Name of the database to restore (as it appears in the backup file). Required to determine file names.
 
.PARAMETER NewDatabaseName
Optional: New name for the database after the restore. If specified, logical file names are
adjusted accordingly (physical files use the new name as base).
 
.PARAMETER NewDatabaseFilePath
Optional: Target directory for database files (.mdf, .ndf). If not specified, the default
directory of the target instance is used (BackupDirectory or DefaultFile).
 
.PARAMETER NewLogFilePath
Optional: Target directory for the log file (.ldf). If not specified, the default directory
of the target instance is used.
 
.PARAMETER BackupBeforeRestore
Optional: Creates a full backup of the existing database before the restore (if present) - AG or
not, this behaves identically, always running against the working instance (the AG's Primary for
an AG-managed database).
The backup is stored in the default backup directory named "DatabaseName_preRestore_YYYYMMDD_HHmmss.bak".
 
.PARAMETER NoUserExport
Optional: Skips export of database users (users are always exported by default).
The export file is stored temporarily in the %TEMP% directory.
 
.PARAMETER KeepAlwaysOn
Optional. If the database is currently part of an AG, it is not removed from the AG - and since a
restore is not possible while still an AG member, the run aborts instead (only useful if the
database is actually already outside the AG despite -KeepAlwaysOn being set). If the database is
NOT currently an AG member, this switch instead opts it out of the automatic single-AG auto-join
described under AvailabilityGroupName below, leaving it standalone on purpose.
 
.PARAMETER AvailabilityGroupName
Optional: Explicitly declares which AG the database belongs to (or should end up in after the
restore), instead of relying solely on live AG-membership/instance detection at the start of the
run. Use this when the database was already removed from the AG by a previous, incompletely
finished run (so it is no longer auto-detected as an AG member), when restoring a brand-new
database straight into an existing AG, or when the instance has more than one AG (auto-detection
only works when there is exactly one). When set, the restore is always treated as AG-aware:
secondaries are cleaned up and the database is rejoined (with seeding) at the end, exactly as if
live detection had found it - regardless of whether the database is currently an AG member.
 
Policy note: even without this parameter, a database that is not currently in any AG will still
be added to the instance's AG automatically if the instance has exactly one - restoring a
database is not allowed to silently leave it standalone on an AG-capable instance. Use
-KeepAlwaysOn to opt out of that auto-join deliberately.
 
.PARAMETER WithNoRecovery
Optional: Performs the restore with NORECOVERY so the database remains in restoring state
(for additional log backups). By default RECOVERY is used (database online).
 
.PARAMETER ContinueWithNoRecovery
Optional: When set, the last restore is also performed with NORECOVERY (e.g. when
additional backups are to be applied manually).
 
.PARAMETER ForceSingleUser
Forces the database into single-user mode before the restore (even if no active connections
are detected). By default only switches when there are active connections.
 
.PARAMETER NoRejoinAvailabilityGroup
Optional: Whenever the function has determined the database should be AG-managed (it was already
an AG member, -AvailabilityGroupName was given, or the instance's single AG was auto-detected), it
is by default automatically (re-)added to that AG afterwards (Add-DbaAgDatabase with SeedingMode
Automatic, which also seeds the secondaries). Use this switch to suppress just the actual join/
rejoin while still doing the rest (secondary cleanup etc.), leaving the database outside the AG
after the restore.
 
.PARAMETER EnableException
Switch to allow exceptions to pass through (by default errors are logged and returned as objects).
 
.PARAMETER Confirm
Request confirmation before critical actions (removing from AG, restore).
 
.PARAMETER WhatIf
Shows what would happen without making changes.
 
.EXAMPLE
# Simple restore of a full backup file
Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -BackupFile "D:\Backup\AdventureWorks.bak" -DatabaseName "AdventureWorks"
 
.EXAMPLE
# Restore with Full + Diff + Logs
$backupSequence = @(
    "D:\Backup\AdventureWorks_Full.bak",
    "D:\Backup\AdventureWorks_Diff.bak",
    "D:\Backup\AdventureWorks_Log1.trn",
    "D:\Backup\AdventureWorks_Log2.trn"
)
Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -BackupFiles $backupSequence -DatabaseName "AdventureWorks"
 
.EXAMPLE
# Restore with new name and forced Single-User mode
Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -BackupFile "D:\Backup\OldDB.bak" -DatabaseName "OldDB" -NewDatabaseName "NewDB" -ForceSingleUser
 
.EXAMPLE
# Retry after a previous run already removed the database from the AG but did not get to
# rejoin it (crash, network blip, etc.) - the database is no longer auto-detected as an AG
# member, so force it explicitly to guarantee the secondaries get reseeded.
Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -BackupFile "D:\Backup\Arena.bak" -DatabaseName "Arena" -AvailabilityGroupName "AG_Prod"
 
.EXAMPLE
# "NewApp" was never an AG member. SQL01 has exactly one AG, so it is auto-detected and the
# restored database is automatically joined to it (with seeding) - no extra parameter needed.
# The actual restore runs against the AG's Primary, even if SQL01 happens to be a Secondary.
Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -BackupFile "D:\Backup\NewApp.bak" -DatabaseName "NewApp"
 
.EXAMPLE
# Same as above, but this restore is a deliberate standalone scratch copy that must NOT join
# the instance's AG.
Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -BackupFile "D:\Backup\NewApp.bak" -DatabaseName "NewApp_scratch" -KeepAlwaysOn
 
.NOTES
Requires dbatools module, Invoke-sqmLogging, Get-sqmConfig, Set-sqmSqlPolicyState.
The function assumes that the executing login has sysadmin rights on the target instance and all secondary replicas.
#>

function Invoke-sqmRestoreDatabase
{
    [CmdletBinding(DefaultParameterSetName = 'SingleFile', SupportsShouldProcess = $true, ConfirmImpact = 'None')]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]$SqlInstance,
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$SqlCredential,
        [Parameter(Mandatory = $true, ParameterSetName = 'SingleFile')]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string[]]$BackupFile,
        [Parameter(Mandatory = $true, ParameterSetName = 'Sequence')]
        [string[]]$BackupFiles,
        [Parameter(Mandatory = $true)]
        [string]$DatabaseName,
        [Parameter(Mandatory = $false)]
        [string]$NewDatabaseName,
        [Parameter(Mandatory = $false)]
        [string]$NewDatabaseFilePath,
        [Parameter(Mandatory = $false)]
        [string]$NewLogFilePath,
        [Parameter(Mandatory = $false)]
        [switch]$BackupBeforeRestore,
        [Parameter(Mandatory = $false)]
        [switch]$NoUserExport,
        [Parameter(Mandatory = $false)]
        [switch]$KeepAlwaysOn,
        [Parameter(Mandatory = $false)]
        [string]$AvailabilityGroupName,
        [Parameter(Mandatory = $false)]
        [switch]$WithNoRecovery,
        [Parameter(Mandatory = $false)]
        [switch]$ContinueWithNoRecovery,
        [Parameter(Mandatory = $false)]
        [switch]$ForceSingleUser,
        [Parameter(Mandatory = $false)]
        [switch]$NoRejoinAvailabilityGroup,
        [Parameter(Mandatory = $false)]
        [switch]$EnableException
    )

    begin
    {
        $functionName = $MyInvocation.MyCommand.Name

        if (-not $PSBoundParameters.ContainsKey('SqlInstance') -or [string]::IsNullOrWhiteSpace($SqlInstance))
        {
            $SqlInstance = $env:COMPUTERNAME
            Write-Verbose "Keine SqlInstance angegeben. Verwende Standard: $SqlInstance"
        }

        if (-not $script:dbatoolsAvailable)
        {
            $errMsg = "dbatools-Modul nicht gefunden. Bitte installieren: Install-Module dbatools"
            Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
            throw $errMsg
        }

        Invoke-sqmLogging -Message "Starte $functionName auf Instanz: $SqlInstance fuer Datenbank '$DatabaseName'" -FunctionName $functionName -Level "INFO"

        # Eventlog-Quelle fuer AG-relevante Aktionen (dieselbe Quelle wie Repair-sqmAlwaysOnDatabases,
        # damit Restore- und Repair-Vorgaenge im selben Event-Log-Kanal auftauchen). Ein fehlgeschlagenes
        # Rejoin/AutoSeed nach einem Restore ist eine kritische Betriebsstoerung und darf nicht nur im
        # sqmSQLTool-Log verschwinden, sondern muss ueber das Eventlog (Monitoring/Alerting) sichtbar sein.
        #
        # WICHTIG: [System.Diagnostics.EventLog]::SourceExists() MUSS selbst in try/catch stehen, nicht
        # nur das New-EventLog. Existiert die Quelle noch nicht, durchsucht SourceExists() ALLE
        # Event-Logs - inklusive des Security-Logs, dessen Lesen erhoehte Rechte erfordert. Laeuft die
        # Funktion unter einem niedrig privilegierten Konto (z.B. dem SQL-Agent-Dienstkonto
        # 'NT SERVICE\SQLSERVERAGENT' in einem Agent-Job), wirft SourceExists() eine SecurityException
        # ("Protokolle, auf die kein Zugriff moeglich war: Security"). Diese darf NIEMALS den ganzen
        # Restore abbrechen - die Eventlog-Integration ist nur Best-Effort-Monitoring. Die spaeteren
        # Write-EventLog-Aufrufe sind ohnehin -ErrorAction SilentlyContinue; kann die Quelle hier nicht
        # geprueft/angelegt werden, verpuffen sie einfach.
        $agEventLogSource = "sqmAlwaysOn"
        try
        {
            if (-not [System.Diagnostics.EventLog]::SourceExists($agEventLogSource))
            {
                New-EventLog -LogName Application -Source $agEventLogSource -ErrorAction Stop
            }
        }
        catch
        {
            Invoke-sqmLogging -Message "Eventlog-Quelle '$agEventLogSource' konnte nicht geprueft/erstellt werden (Restore laeuft trotzdem weiter): $($_.Exception.Message)" -FunctionName $functionName -Level "WARNING"
        }

        $results = @()
        $tempDir = [System.IO.Path]::GetTempPath()
        $userExportFile = Join-Path $tempDir "UserExport_${DatabaseName}_$(Get-Date -Format 'yyyyMMddHHmmss').sql"
        $isAGDatabase = $false
        $availabilityGroup = $null
        $primaryInstance = $null
        $workInstance = $null
        $secondaryInstances = @()
        $wasSingleUser = $false
        $originalDbStatus = $null
        $restoreSucceeded = $false
        # Sicherer Default, falls die Funktion vor Schritt 5 abbricht (z.B. Fehler bei AG-Entfernen)
        # und $wasSingleUser bereits true ist (z.B. durch die Bereits-Single-User-Normalisierung in
        # Schritt 1) - der finally-Block braucht fuer den MULTI_USER-Revert einen gueltigen Namen,
        # nicht $null. Schritt 5 ueberschreibt dies bei Bedarf mit $NewDatabaseName.
        $finalDbName = $DatabaseName

        # Policy-Kontrolle
        $policyName = Get-sqmConfig -Key 'DefaultPolicy' 3>$null
        $policyWasEnabled = $false
        $policyDeactivated = $false

        # Bestimme die Liste der Backup-Dateien je nach Parametersatz
        $backupFileList = if ($PSCmdlet.ParameterSetName -eq 'Sequence') { $BackupFiles }
        else { $BackupFile }
    }

    process
    {
        try
        {
            # ---- 1. Einstiegs-Instanz verbinden und AG-Zugehoerigkeit/-Topologie ermitteln ----
            # AG-Katalogsicht (Mitgliedschaft, Replica-Topologie) ist clusterweit verfuegbar und wird
            # daher ueber die Einstiegs-Instanz ($SqlInstance) abgefragt. Die eigentliche Arbeit
            # (Backup, User-Export, Single-User, Restore, User-Import, Cleanup) laeuft danach aber
            # IMMER auf der weiter unten ermittelten Arbeits-Instanz ($workInstance) - bei einer
            # AG-Datenbank ist das grundsaetzlich die Primary, alles andere ergibt keinen Sinn
            # (Restore/DDL gegen eine Secondary ist nicht moeglich). Ab dort verhaelt sich die
            # Funktion fuer AG- und Nicht-AG-Datenbanken identisch - nur die AG-spezifischen Schritte
            # (Entfernen/Secondary-Cleanup/Rejoin) kommen bei einer AG-Datenbank zusaetzlich dazu.
            $server = Connect-DbaInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential -ErrorAction Stop

            # Pruefen, ob die Datenbank in einer AG ist. Laeuft UNABHAENGIG davon, ob die Datenbank
            # unter diesem Namen bereits existiert: eine Datenbank, die noch nie existiert hat
            # (Erst-Restore einer neuen Anwendung), muss der Richtlinie "alle Datenbanken muessen in
            # AlwaysOn sein" genauso unterliegen wie eine bereits vorhandene, standalone Datenbank.
            # WICHTIG: Get-DbaAvailabilityGroup hat KEINEN -Database-Parameter. Ein -Database loest einen
            # terminierenden Parameter-Binding-Fehler aus, den -ErrorAction SilentlyContinue NICHT abfaengt.
            # Daher Mitgliedschaft ueber Get-DbaAgDatabase pruefen und das AG-Objekt (mit .Name) anschliessend
            # ueber den AG-Namen nachladen.
            $agDbCheck = Get-DbaAgDatabase -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $DatabaseName -ErrorAction SilentlyContinue
            if ($agDbCheck)
            {
                $isAGDatabase = $true
                $agName = ($agDbCheck | Select-Object -First 1).AvailabilityGroup
                if ($AvailabilityGroupName -and $AvailabilityGroupName -ne $agName)
                {
                    Invoke-sqmLogging -Message "Hinweis: -AvailabilityGroupName '$AvailabilityGroupName' weicht von der live erkannten AG '$agName' ab - verwende die live erkannte AG." -FunctionName $functionName -Level "WARNING"
                }
                $availabilityGroup = Get-DbaAvailabilityGroup -SqlInstance $SqlInstance -SqlCredential $SqlCredential -AvailabilityGroup $agName -ErrorAction SilentlyContinue
                Invoke-sqmLogging -Message "Datenbank ist Mitglied der AG '$($availabilityGroup.Name)'." -FunctionName $functionName -Level "INFO"
                if (-not $KeepAlwaysOn)
                {
                    Invoke-sqmLogging -Message "Die Datenbank wird aus der AG entfernt (einschliesslich sekundaerer Replikate)." -FunctionName $functionName -Level "INFO"
                }
                else
                {
                    Invoke-sqmLogging -Message "KeepAlwaysOn ist gesetzt - die Datenbank verbleibt in der AG. Ein Restore ist in einer AG nicht moeglich, daher wird der Vorgang abgebrochen." -FunctionName $functionName -Level "ERROR"
                    throw "Datenbank ist Teil einer AG und KeepAlwaysOn wurde angegeben. Restore nicht moeglich."
                }
            }
            elseif ($AvailabilityGroupName)
            {
                # Datenbank ist AKTUELL kein AG-Mitglied (z.B. weil ein vorheriger, unvollstaendig
                # abgeschlossener Lauf sie bereits aus der AG entfernt hat), aber der Aufrufer hat
                # explizit angegeben, dass sie zur AG gehoert/gehoeren soll. Ohne diesen Parameter
                # wuerde die Funktion die AG-Zugehoerigkeit hier NICHT mehr erkennen und den Rest
                # des Laufs (Secondary-Cleanup, Rejoin/Reseed am Ende) stillschweigend ueberspringen -
                # genau das darf bei einer AG-Datenbank nie passieren.
                $availabilityGroup = Get-DbaAvailabilityGroup -SqlInstance $SqlInstance -SqlCredential $SqlCredential -AvailabilityGroup $AvailabilityGroupName -ErrorAction SilentlyContinue
                if (-not $availabilityGroup)
                {
                    throw "Availability Group '$AvailabilityGroupName' wurde auf '$SqlInstance' nicht gefunden."
                }
                $isAGDatabase = $true
                Invoke-sqmLogging -Message "Datenbank ist aktuell KEIN Mitglied der AG '$AvailabilityGroupName' (vermutlich bereits entfernt), wird aber laut -AvailabilityGroupName als AG-Datenbank behandelt: Secondary-Cleanup und Rejoin/Reseed werden trotzdem durchgefuehrt." -FunctionName $functionName -Level "WARNING"
            }
            elseif (-not $KeepAlwaysOn)
            {
                # Datenbank ist aktuell in KEINER AG (existierte evtl. noch nie unter diesem Namen)
                # und der Aufrufer hat keine AG explizit angegeben. Unternehmensrichtlinie: JEDE
                # Datenbank auf einer Instanz mit AG muss in AlwaysOn sein - ein Restore darf niemals
                # stillschweigend eine standalone Datenbank zuruecklassen, wenn die Instanz eine AG
                # hat (das gilt auch fuer den allerersten Restore einer neuen Anwendung). Hat die
                # Instanz genau EINE AG, wird diese automatisch verwendet. Bei 0 AGs gibt es nichts
                # beizutreten (z.B. Non-Cluster-Instanz) - bleibt standalone. Bei 2+ AGs ist die
                # Zuordnung nicht eindeutig - Abbruch, -AvailabilityGroupName muss explizit angegeben
                # werden.
                $instanceAgs = @(Get-DbaAvailabilityGroup -SqlInstance $SqlInstance -SqlCredential $SqlCredential -ErrorAction SilentlyContinue)
                if ($instanceAgs.Count -eq 1)
                {
                    $availabilityGroup = $instanceAgs[0]
                    $isAGDatabase = $true
                    Invoke-sqmLogging -Message "Datenbank ist aktuell in keiner AG, Instanz hat aber genau eine AG '$($availabilityGroup.Name)' - wird nach dem Restore automatisch dieser AG hinzugefuegt (Richtlinie: alle Datenbanken muessen in AlwaysOn sein)." -FunctionName $functionName -Level "WARNING"
                }
                elseif ($instanceAgs.Count -gt 1)
                {
                    $agNames = ($instanceAgs | Select-Object -ExpandProperty Name) -join ', '
                    Invoke-sqmLogging -Message "Datenbank ist in keiner AG und Instanz '$SqlInstance' hat mehrere AGs ($agNames) - nicht eindeutig, welche AG die Datenbank erhalten soll." -FunctionName $functionName -Level "ERROR"
                    throw "Instanz '$SqlInstance' hat mehrere Availability Groups ($agNames). Bitte -AvailabilityGroupName explizit angeben."
                }
                else
                {
                    Invoke-sqmLogging -Message "Datenbank ist in keiner AG und Instanz '$SqlInstance' hat keine Availability Group - Datenbank bleibt standalone." -FunctionName $functionName -Level "INFO"
                }
            }

            # ---- Arbeits-Instanz bestimmen: bei einer AG-Datenbank IMMER die Primary ----
            if ($isAGDatabase)
            {
                $replicas = Get-DbaAgReplica -SqlInstance $SqlInstance -SqlCredential $SqlCredential -AvailabilityGroup $availabilityGroup.Name -ErrorAction Stop

                # Primary-Ermittlung ueber AvailabilityGroup.PrimaryReplicaServerName statt ueber
                # Get-DbaAgReplica + "Role -eq 'Primary'"-Filter: PrimaryReplicaServerName ist eine
                # eigene, dedizierte SMO-Eigenschaft der AG selbst und damit die verlaessliche Quelle;
                # die einzelnen Replica-Objekte koennen ihre Role transient anders melden (z.B.
                # 'Resolving'), wodurch ein Role-Filter leer zurueckkommen kann.
                $primaryInstance = $availabilityGroup.PrimaryReplicaServerName

                # Kurzname (ohne Domaenen-Suffix) fuer den Vergleich - PrimaryReplicaServerName kann
                # als FQDN, mit anderer Gross-/Kleinschreibung oder anders formatiert zurueckkommen als
                # die vom Aufrufer uebergebene -SqlInstance, obwohl es dieselbe Maschine ist. Ist es
                # dieselbe Maschine, wird bewusst die EXAKTE, vom Aufrufer uebergebene Zeichenkette
                # weiterverwendet statt der von der AG gemeldeten - ein reiner Formatunterschied kann
                # sonst (z.B. bei Kerberos-Delegation) dazu fuehren, dass Export-DbaUser beim
                # Aufzaehlen von Berechtigungen mit einer generischen SMO-/T-SQL-Ausnahme fehlschlaegt,
                # obwohl genau dieselbe Instanz gemeint ist und der Restore mit der Original-Zeichenkette
                # bereits nachweislich funktioniert hat.
                $primaryShortName = ($primaryInstance -split '[.\\]')[0]
                $sqlInstanceShortName = ($SqlInstance -split '[.\\]')[0]

                # Immer geloggt (nicht nur bei erkanntem Unterschied) - falls trotz gleicher Maschine
                # noch etwas schiefgeht, zeigt das Log exakt, welche rohen Zeichenketten verglichen
                # wurden, statt hinterher raten zu muessen.
                Invoke-sqmLogging -Message "Primary-Abgleich: PrimaryReplicaServerName='$primaryInstance' (Kurzname='$primaryShortName') vs. -SqlInstance='$SqlInstance' (Kurzname='$sqlInstanceShortName')." -FunctionName $functionName -Level "DEBUG"

                if ([string]::IsNullOrWhiteSpace($primaryInstance))
                {
                    Invoke-sqmLogging -Message "AG '$($availabilityGroup.Name)': PrimaryReplicaServerName ist leer (AG evtl. gerade im Failover) - falle zurueck auf verbundene Instanz '$SqlInstance'." -FunctionName $functionName -Level "WARNING"
                    $primaryInstance = $SqlInstance
                }
                elseif ($primaryShortName -ieq $sqlInstanceShortName)
                {
                    # Selbe Maschine wie -SqlInstance (nur evtl. anders formatiert) - Original-Zeichenkette
                    # beibehalten statt der von der AG gemeldeten.
                    $primaryInstance = $SqlInstance
                }
                else
                {
                    Invoke-sqmLogging -Message "Aktuelle Instanz ist nicht primaer. Alle weiteren Schritte (Backup, Export, Restore, Cleanup) laufen gegen die primaere Instanz '$primaryInstance'." -FunctionName $functionName -Level "INFO"
                }
                $secondaryInstances = $replicas | Where-Object { ($_.Name -split '[.\\]')[0] -ine $primaryShortName } | Select-Object -ExpandProperty Name
                $workInstance = $primaryInstance
            }
            else
            {
                $workInstance = $SqlInstance
            }

            # ---- Vorbereitung: Policy temporaer deaktivieren (auf der Arbeits-Instanz) ----
            if (-not [string]::IsNullOrWhiteSpace($policyName))
            {
                try
                {
                    $policyObj = Get-DbaPbmPolicy -SqlInstance $workInstance -SqlCredential $SqlCredential -Policy $policyName -ErrorAction SilentlyContinue
                    if ($policyObj -and $policyObj.Policy.Enabled)
                    {
                        $policyWasEnabled = $true
                        if ($PSCmdlet.ShouldProcess($workInstance, "Temporaer Policy '$policyName' deaktivieren fuer Restore-Operation"))
                        {
                            Invoke-sqmLogging -Message "Deaktiviere Policy '$policyName' temporaer." -FunctionName $functionName -Level "INFO"
                            Set-sqmSqlPolicyState -SqlInstance $workInstance -SqlCredential $SqlCredential -Policy $policyName -State Disable -EnableException:$EnableException -Confirm:$false
                            $policyDeactivated = $true
                            $results += [PSCustomObject]@{ Action = "PolicyTemporaryDisable"; Status = "Success"; Message = "Policy '$policyName' deaktiviert." }
                        }
                        else
                        {
                            $results += [PSCustomObject]@{ Action = "PolicyTemporaryDisable"; Status = "Skipped"; Message = "WhatIf - Policy-Deaktivierung uebersprungen." }
                        }
                    }
                }
                catch
                {
                    Invoke-sqmLogging -Message "Warnung: Konnte Policy '$policyName' nicht pruefen: $($_.Exception.Message)" -FunctionName $functionName -Level "WARNING"
                }
            }

            # ---- Datenbank-Status auf der Arbeits-Instanz ermitteln ----
            $workServer = if ($workInstance -eq $SqlInstance) { $server }
            else { Connect-DbaInstance -SqlInstance $workInstance -SqlCredential $SqlCredential -ErrorAction Stop }
            $targetDb = $workServer.Databases[$DatabaseName]
            $dbExists = $targetDb -ne $null

            if ($dbExists)
            {
                Invoke-sqmLogging -Message "Datenbank '$DatabaseName' existiert auf $workInstance." -FunctionName $functionName -Level "INFO"

                # Datenbank kann bereits VOR diesem Aufruf in SINGLE_USER/RESTRICTED_USER stehen (z.B.
                # Rest eines vorherigen, abgebrochenen Restores oder manuell durch einen DBA gesetzt).
                # In dem Fall haelt eine fremde Session bereits den einzigen verfuegbaren Connection-Slot
                # - jeder eigene Connect (allen voran Export-DbaUser in Schritt 3, das eine eigene
                # SMO-Verbindung braucht) wuerde dann sofort mit "database is already open and can only
                # have one user at a time" fehlschlagen, noch bevor unser eigener Single-User-Schritt
                # (Schritt 3b) ueberhaupt drankommt. Deshalb hier sofort auf MULTI_USER zuruecksetzen
                # (WITH ROLLBACK IMMEDIATE wirft die fremde Session raus) - $wasSingleUser wird bereits
                # hier gesetzt, damit die Datenbank am Ende garantiert wieder MULTI_USER ist, auch wenn
                # Schritt 3b selbst keinen Grund mehr sieht, aktiv zu werden.
                $originalDbStatus = $targetDb.UserAccess
                if ($originalDbStatus -ne [Microsoft.SqlServer.Management.Smo.DatabaseUserAccess]::Multiple)
                {
                    $normalizeAction = "Datenbank '$DatabaseName' ist bereits im Modus '$originalDbStatus' - setze auf MULTI_USER zurueck (fremde Session wird getrennt)"
                    Invoke-sqmLogging -Message $normalizeAction -FunctionName $functionName -Level "WARNING"
                    if ($PSCmdlet.ShouldProcess($DatabaseName, $normalizeAction))
                    {
                        try
                        {
                            Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential -Database master `
                                -Query "ALTER DATABASE [$DatabaseName] SET MULTI_USER WITH ROLLBACK IMMEDIATE;" -ErrorAction Stop
                            $wasSingleUser = $true
                            $targetDb.Refresh()
                            Invoke-sqmLogging -Message "Datenbank '$DatabaseName' auf MULTI_USER zurueckgesetzt." -FunctionName $functionName -Level "INFO"
                            $results += [PSCustomObject]@{ Action = "NormalizeToMultiUser"; Status = "Success"; Message = "War '$originalDbStatus', auf MULTI_USER zurueckgesetzt." }
                        }
                        catch
                        {
                            $errMsg = "Fehler beim Zuruecksetzen auf MULTI_USER: $($_.Exception.Message)"
                            Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                            if ($EnableException) { throw }
                            $results += [PSCustomObject]@{ Action = "NormalizeToMultiUser"; Status = "Failed"; Message = $errMsg }
                            return
                        }
                    }
                    else
                    {
                        $results += [PSCustomObject]@{ Action = "NormalizeToMultiUser"; Status = "Skipped"; Message = "WhatIf - Zuruecksetzen uebersprungen." }
                    }
                }

                # Aktive Verbindungen hier nur ermitteln (fuer spaeteren Single-User-Schritt) - NICHT
                # schon jetzt in Single-User versetzen, das muss erst NACH dem User-Export (Schritt 3)
                # passieren (siehe dort).
                $activeConnections = $targetDb.ActiveConnections
            }
            else
            {
                Invoke-sqmLogging -Message "Datenbank '$DatabaseName' existiert nicht auf $workInstance." -FunctionName $functionName -Level "INFO"
            }

            # ---- 2. Optional: Backup der vorhandenen Datenbank ----
            # AG oder nicht - identisch: laeuft immer gegen die Arbeits-Instanz, solange die
            # Datenbank dort existiert. Frueher wurde dieser Schritt fuer AG-Datenbanken komplett
            # uebersprungen, was verwirrend war, wenn -BackupBeforeRestore explizit angegeben wurde.
            if ($BackupBeforeRestore -and $dbExists)
            {
                $backupFileName = "${DatabaseName}_preRestore_$(Get-Date -Format 'yyyyMMdd_HHmmss').bak"
                $backupFileFull = Join-Path (Get-DbaDefaultPath -SqlInstance $workInstance -SqlCredential $SqlCredential).Backup $backupFileName
                $backupParams = @{
                    SqlInstance   = $workInstance
                    SqlCredential = $SqlCredential
                    Database      = $DatabaseName
                    Path          = $backupFileFull
                    Type          = 'Full'
                    ErrorAction   = 'Stop'
                }
                if ($PSCmdlet.ShouldProcess($DatabaseName, "Backup der Datenbank '$DatabaseName' nach $backupFileFull"))
                {
                    try
                    {
                        Invoke-sqmLogging -Message "Erstelle Backup der vorhandenen Datenbank: $backupFileFull" -FunctionName $functionName -Level "INFO"
                        Backup-DbaDatabase @backupParams
                        Invoke-sqmLogging -Message "Backup erfolgreich." -FunctionName $functionName -Level "INFO"
                        $results += [PSCustomObject]@{ Action = "PreRestoreBackup"; Status = "Success"; Message = "Backup erstellt: $backupFileFull" }
                    }
                    catch
                    {
                        $errMsg = "Fehler beim Backup vor dem Restore: $($_.Exception.Message)"
                        Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                        if ($EnableException) { throw }
                        $results += [PSCustomObject]@{ Action = "PreRestoreBackup"; Status = "Failed"; Message = $errMsg }
                        return
                    }
                }
                else
                {
                    $results += [PSCustomObject]@{ Action = "PreRestoreBackup"; Status = "Skipped"; Message = "WhatIf - Backup uebersprungen." }
                }
            }

            # ---- 3. Export der Datenbank-User (immer, es sei denn NoUserExport) ----
            if (-not $NoUserExport -and $dbExists)
            {
                if ($PSCmdlet.ShouldProcess($DatabaseName, "Export der Datenbank-User nach $userExportFile"))
                {
                    try
                    {
                        Invoke-sqmLogging -Message "Exportiere User der Datenbank '$DatabaseName' nach $userExportFile" -FunctionName $functionName -Level "INFO"
                        Export-DbaUser -SqlInstance $workInstance -SqlCredential $SqlCredential -Database $DatabaseName -FilePath $userExportFile -ErrorAction Stop
                        Invoke-sqmLogging -Message "User-Export erfolgreich." -FunctionName $functionName -Level "INFO"
                        $results += [PSCustomObject]@{ Action = "UserExport"; Status = "Success"; Message = "Exportdatei: $userExportFile" }
                    }
                    catch
                    {
                        $errMsg = "Fehler beim Export der User: $($_.Exception.Message)"
                        Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                        if ($EnableException) { throw }
                        $results += [PSCustomObject]@{ Action = "UserExport"; Status = "Failed"; Message = $errMsg }
                        return
                    }
                }
                else
                {
                    $results += [PSCustomObject]@{ Action = "UserExport"; Status = "Skipped"; Message = "WhatIf - User-Export uebersprungen." }
                }
            }

            # ---- 3b. Datenbank in Single-User-Modus versetzen, falls noetig ----
            # WICHTIG: Muss NACH dem User-Export (Schritt 3) laufen, nicht davor. Export-DbaUser
            # oeffnet fuer das Scripting der Objekt-/Berechtigungs-DDL eine eigene SMO-Verbindung zur
            # Datenbank; laeuft die Datenbank zu diesem Zeitpunkt schon in SINGLE_USER, schlaegt dieser
            # zweite Connect mit "Database '<db>' is already open and can only have one user at a
            # time" fehl (Export-DbaUser faengt den Fehler intern ab und meldet ihn nur als WARNING,
            # der Export bricht dann aber unvollstaendig/leer ab).
            if ($dbExists -and ($activeConnections -gt 0 -or $ForceSingleUser))
            {
                if ($activeConnections -gt 0)
                {
                    Invoke-sqmLogging -Message "Datenbank '$DatabaseName' hat $activeConnections aktive Verbindungen. Setze in Single-User-Modus." -FunctionName $functionName -Level "INFO"
                }
                else
                {
                    Invoke-sqmLogging -Message "Erzwinge Single-User-Modus fuer Datenbank '$DatabaseName'." -FunctionName $functionName -Level "INFO"
                }
                $setSingleUserAction = "Setze Datenbank '$DatabaseName' in Single-User-Modus"
                if ($PSCmdlet.ShouldProcess($DatabaseName, $setSingleUserAction))
                {
                    try
                    {
                        $singleUserQuery = "ALTER DATABASE [$DatabaseName] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;"
                        Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential -Database master -Query $singleUserQuery -ErrorAction Stop
                        $wasSingleUser = $true
                        Invoke-sqmLogging -Message "Datenbank '$DatabaseName' jetzt im Single-User-Modus." -FunctionName $functionName -Level "INFO"
                        $results += [PSCustomObject]@{ Action = "SetSingleUser"; Status = "Success"; Message = "Datenbank in Single-User versetzt." }
                    }
                    catch
                    {
                        $errMsg = "Fehler beim Setzen des Single-User-Modus: $($_.Exception.Message)"
                        Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                        if ($EnableException) { throw }
                        $results += [PSCustomObject]@{ Action = "SetSingleUser"; Status = "Failed"; Message = $errMsg }
                        return
                    }
                }
                else
                {
                    $results += [PSCustomObject]@{ Action = "SetSingleUser"; Status = "Skipped"; Message = "WhatIf - Single-User uebersprungen." }
                }
            }
            elseif ($dbExists)
            {
                Invoke-sqmLogging -Message "Datenbank '$DatabaseName' hat keine aktiven Verbindungen." -FunctionName $functionName -Level "INFO"
            }

            # ---- 4. Wenn AlwaysOn: Datenbank aus der AG entfernen (primaer) und von sekundaeren Replikaten loeschen ----
            # Primary/Secondaries wurden bereits oben (Arbeits-Instanz-Ermittlung) bestimmt.
            if ($isAGDatabase -and -not $KeepAlwaysOn)
            {
                if (-not $agDbCheck)
                {
                    # Ueber -AvailabilityGroupName erzwungen: Datenbank ist bereits kein AG-Mitglied mehr
                    # (z.B. Rest eines vorherigen, abgebrochenen Laufs) - nichts zu entfernen, aber
                    # Secondary-Cleanup und Rejoin/Reseed unten laufen trotzdem weiter.
                    Invoke-sqmLogging -Message "Datenbank ist aktuell kein AG-Mitglied mehr - AG-Entfernen wird uebersprungen." -FunctionName $functionName -Level "INFO"
                    $results += [PSCustomObject]@{ Action = "RemoveFromAG"; Status = "NotNeeded"; Message = "Datenbank war zu Laufbeginn bereits kein AG-Mitglied mehr." }
                }
                else
                {
                    $removeAgAction = "Entferne Datenbank '$DatabaseName' aus der AG '$($availabilityGroup.Name)'"
                    if ($PSCmdlet.ShouldProcess($DatabaseName, $removeAgAction))
                    {
                        try
                        {
                            Invoke-sqmLogging -Message $removeAgAction -FunctionName $functionName -Level "INFO"
                            Remove-DbaAgDatabase -SqlInstance $primaryInstance -SqlCredential $SqlCredential -AvailabilityGroup $availabilityGroup.Name -Database $DatabaseName -ErrorAction Stop
                            Invoke-sqmLogging -Message "Datenbank erfolgreich aus AG entfernt." -FunctionName $functionName -Level "INFO"
                            $results += [PSCustomObject]@{ Action = "RemoveFromAG"; Status = "Success"; Message = "Datenbank aus AG entfernt." }
                        }
                        catch
                        {
                            $errMsg = "Fehler beim Entfernen aus AG: $($_.Exception.Message)"
                            Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                            Write-EventLog -LogName Application -Source $agEventLogSource -EventId 9010 -EntryType Error `
                                -Message "Invoke-sqmRestoreDatabase: Entfernen von '$DatabaseName' aus AG '$($availabilityGroup.Name)' auf '$primaryInstance' fehlgeschlagen: $errMsg" -ErrorAction SilentlyContinue
                            if ($EnableException) { throw }
                            $results += [PSCustomObject]@{ Action = "RemoveFromAG"; Status = "Failed"; Message = $errMsg }
                            return
                        }
                    }
                    else
                    {
                        $results += [PSCustomObject]@{ Action = "RemoveFromAG"; Status = "Skipped"; Message = "WhatIf - Entfernen aus AG uebersprungen." }
                    }
                }

                foreach ($secondary in $secondaryInstances)
                {
                    $removeDbAction = "Loesche Datenbank '$DatabaseName' auf sekundaerem Knoten '$secondary'"
                    if ($PSCmdlet.ShouldProcess($DatabaseName, $removeDbAction))
                    {
                        try
                        {
                            Invoke-sqmLogging -Message $removeDbAction -FunctionName $functionName -Level "INFO"
                            $secondaryServer = Connect-DbaInstance -SqlInstance $secondary -SqlCredential $SqlCredential -ErrorAction Stop
                            if ($secondaryServer.Databases[$DatabaseName] -and $secondaryServer.Databases[$DatabaseName].IsAccessible)
                            {
                                Remove-DbaDatabase -SqlInstance $secondary -SqlCredential $SqlCredential -Database $DatabaseName -Confirm:$false -ErrorAction Stop
                                Invoke-sqmLogging -Message "Datenbank auf '$secondary' geloescht." -FunctionName $functionName -Level "INFO"
                                $results += [PSCustomObject]@{ Action = "RemoveFromSecondary"; Target = $secondary; Status = "Success"; Message = "Datenbank auf sekundaerem Knoten geloescht." }
                            }
                            else
                            {
                                Invoke-sqmLogging -Message "Datenbank auf '$secondary' nicht vorhanden." -FunctionName $functionName -Level "VERBOSE"
                            }
                        }
                        catch
                        {
                            $errMsg = "Fehler beim Loeschen auf '$secondary': $($_.Exception.Message)"
                            Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                            if ($EnableException) { throw }
                            $results += [PSCustomObject]@{ Action = "RemoveFromSecondary"; Target = $secondary; Status = "Failed"; Message = $errMsg }
                        }
                    }
                    else
                    {
                        $results += [PSCustomObject]@{ Action = "RemoveFromSecondary"; Target = $secondary; Status = "Skipped"; Message = "WhatIf - Loeschen uebersprungen." }
                    }
                }
            }

            # ---- 5. Restore der Datenbank(en) ----
            # Laeuft immer gegen die Arbeits-Instanz ($workInstance) - bei einer AG-Datenbank ist das
            # die Primary, alles andere ergibt keinen Sinn (Restore gegen eine Secondary ist nicht
            # moeglich).
            $finalDbName = if ($NewDatabaseName) { $NewDatabaseName }
            else { $DatabaseName }
            $restoreCount = 0
            $totalFiles = $backupFileList.Count

            foreach ($file in $backupFileList)
            {
                $restoreCount++
                $isLast = ($restoreCount -eq $totalFiles)
                $useRecovery = if ($ContinueWithNoRecovery) { $false }
                elseif ($WithNoRecovery) { $false }
                else { $isLast }

                $restoreParams = @{
                    SqlInstance   = $workInstance
                    SqlCredential = $SqlCredential
                    Path          = $file
                    DatabaseName  = $finalDbName
                    WithReplace   = $true
                    NoRecovery    = (-not $useRecovery)
                    ErrorAction   = 'Stop'
                }
                # Hinweis: Restore-DbaDatabase kennt KEINE Parameter -NewDatabaseName/-DatabaseFilePath/-LogFilePath.
                # Der Zielname (auch ein neuer) wird ueber -DatabaseName ($finalDbName) gesetzt; die physischen
                # Datei-Namen/-Pfade regelt das weiter unten aufgebaute -FileMapping (nutzt NewDatabaseFilePath/
                # NewLogFilePath als Zielverzeichnisse). Damit sind Umbenennen + Verschieben versionsstabil abgedeckt.
                # Fuer alle ausser den ersten Restore (Full) muss der Datenbankname bereits existieren; fuer Log-Restores ist das wichtig
                # Restore-DbaDatabase kann sequenziell verarbeitet werden.

                # Auto-FileMapping: beim ersten Restore (Full) immer FileMapping aus RESTORE FILELISTONLY aufbauen.
                # Das verhindert Pfadkonflikte wenn das Backup bereits vorhandene Dateipfade enthaelt.
                if ($restoreCount -eq 1)
                {
                    try
                    {
                        $safeFilePath = $file.Replace("'", "''")
                        $backupFileListing = Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential `
                            -Database 'master' `
                            -Query "RESTORE FILELISTONLY FROM DISK = N'$safeFilePath'" `
                            -ErrorAction Stop

                        if ($backupFileListing)
                        {
                            $defaultPaths = Get-DbaDefaultPath -SqlInstance $workInstance -SqlCredential $SqlCredential
                            $dataDir = if ($NewDatabaseFilePath) { $NewDatabaseFilePath } else { $defaultPaths.Data }
                            $logDir  = if ($NewLogFilePath)      { $NewLogFilePath }      else { $defaultPaths.Log }

                            $fileMapping = @{}
                            $logIdx = 0
                            $datIdx = 0
                            foreach ($backupFileEntry in $backupFileListing)
                            {
                                $ext   = [System.IO.Path]::GetExtension($backupFileEntry.PhysicalName)
                                $isLog = ($backupFileEntry.Type -eq 'L')
                                $dir   = if ($isLog) { $logDir } else { $dataDir }

                                if ($isLog)
                                {
                                    $logIdx++
                                    $sfx = if ($logIdx -eq 1) { '_log' } else { "_log$logIdx" }
                                }
                                else
                                {
                                    $datIdx++
                                    $sfx = if ($datIdx -eq 1) { '' } else { "_$datIdx" }
                                }

                                $fileMapping[$backupFileEntry.LogicalName] = Join-Path $dir "$finalDbName$sfx$ext"
                                Invoke-sqmLogging -Message "FileMapping: '$($backupFileEntry.LogicalName)' -> '$($fileMapping[$backupFileEntry.LogicalName])'" `
                                    -FunctionName $functionName -Level "INFO"
                            }
                            $restoreParams['FileMapping'] = $fileMapping
                        }
                    }
                    catch
                    {
                        Invoke-sqmLogging -Message "Auto-FileMapping fehlgeschlagen (wird uebersprungen): $($_.Exception.Message)" `
                            -FunctionName $functionName -Level "WARNING"
                    }
                }

                $restoreAction = "Restore von $file ($restoreCount/$totalFiles) fuer Datenbank '$DatabaseName'"
                if ($NewDatabaseName) { $restoreAction += " als '$NewDatabaseName'" }
                if (-not $useRecovery) { $restoreAction += " (NORECOVERY)" }

                if ($PSCmdlet.ShouldProcess($DatabaseName, $restoreAction))
                {
                    try
                    {
                        Invoke-sqmLogging -Message $restoreAction -FunctionName $functionName -Level "INFO"
                        $restoreResult = Restore-DbaDatabase @restoreParams
                        Invoke-sqmLogging -Message "Restore von $file erfolgreich." -FunctionName $functionName -Level "INFO"
                        $results += [PSCustomObject]@{ Action = "RestoreStep"; File = $file; Step = $restoreCount; Status = "Success"; Message = "Wiederhergestellt." }
                    }
                    catch
                    {
                        $errMsg = "Fehler beim Restore von $file : $($_.Exception.Message)"
                        Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                        if ($EnableException) { throw }
                        $results += [PSCustomObject]@{ Action = "RestoreStep"; File = $file; Step = $restoreCount; Status = "Failed"; Message = $errMsg }
                        return
                    }
                }
                else
                {
                    $results += [PSCustomObject]@{ Action = "RestoreStep"; File = $file; Step = $restoreCount; Status = "Skipped"; Message = "WhatIf - Restore uebersprungen." }
                    return
                }
            }

            # Ab hier ist die Datenbank tatsaechlich wiederhergestellt. Der AG-Rejoin (siehe finally-
            # Block) darf danach durch nichts mehr verhindert werden - auch nicht durch einen mit
            # -EnableException durchgereichten Fehler in einem der folgenden (nicht-kritischen)
            # Aufraeumschritte 6-9.
            $restoreSucceeded = $true

            # ---- 6. Nach dem Restore: User wiederherstellen (wenn Export durchgefuehrt) ----
            if (-not $NoUserExport -and (Test-Path $userExportFile))
            {
                $importAction = "Importiere User aus $userExportFile in Datenbank '$finalDbName'"
                if ($PSCmdlet.ShouldProcess($finalDbName, $importAction))
                {
                    try
                    {
                        Invoke-sqmLogging -Message $importAction -FunctionName $functionName -Level "INFO"
                        $sql = Get-Content $userExportFile -Raw
                        Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential -Database $finalDbName -Query $sql -ErrorAction Stop
                        Invoke-sqmLogging -Message "User-Import erfolgreich." -FunctionName $functionName -Level "INFO"
                        $results += [PSCustomObject]@{ Action = "UserImport"; Status = "Success"; Message = "User aus Export wiederhergestellt." }
                    }
                    catch
                    {
                        $errMsg = "Fehler beim Import der User: $($_.Exception.Message)"
                        Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                        if ($EnableException) { throw }
                        $results += [PSCustomObject]@{ Action = "UserImport"; Status = "Failed"; Message = $errMsg }
                    }
                }
                else
                {
                    $results += [PSCustomObject]@{ Action = "UserImport"; Status = "Skipped"; Message = "WhatIf - User-Import uebersprungen." }
                }
            }

            # ---- 7. Verwaiste User reparieren ----
            $orphanFixAction = "Repariere verwaiste User in Datenbank '$finalDbName'"
            if ($PSCmdlet.ShouldProcess($finalDbName, $orphanFixAction))
            {
                try
                {
                    Invoke-sqmLogging -Message $orphanFixAction -FunctionName $functionName -Level "INFO"
                    $repairResult = Repair-DbaDbOrphanUser -SqlInstance $workInstance -SqlCredential $SqlCredential -Database $finalDbName -ErrorAction Stop
                    $repairedCount = if ($repairResult) { @($repairResult).Count } else { 0 }
                    Invoke-sqmLogging -Message "Verwaiste User repariert: $repairedCount." -FunctionName $functionName -Level "INFO"
                    $results += [PSCustomObject]@{ Action = "FixOrphans"; Status = "Success"; Message = "Repair-DbaDbOrphanUser: $repairedCount User repariert." }
                }
                catch
                {
                    $errMsg = "Fehler bei der Reparatur verwaister User: $($_.Exception.Message)"
                    Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                    if ($EnableException) { throw }
                    $results += [PSCustomObject]@{ Action = "FixOrphans"; Status = "Failed"; Message = $errMsg }
                }
            }
            else
            {
                $results += [PSCustomObject]@{ Action = "FixOrphans"; Status = "Skipped"; Message = "WhatIf - Reparatur uebersprungen." }
            }

            # ---- 8. Domaenenfremde Accounts entfernen ----
            $removeOrphanLoginsAction = "Entferne nicht mehr existierende Windows-Logins aus Datenbank '$finalDbName'"
            if ($PSCmdlet.ShouldProcess($finalDbName, $removeOrphanLoginsAction))
            {
                try
                {
                    Invoke-sqmLogging -Message $removeOrphanLoginsAction -FunctionName $functionName -Level "INFO"
                    $query = @"
DECLARE @dbname sysname = DB_NAME();
SELECT dp.name AS UserName
FROM sys.database_principals dp
LEFT JOIN sys.server_principals sp ON dp.sid = sp.sid
WHERE dp.type IN ('U', 'G')
  AND sp.sid IS NULL
  AND dp.name NOT IN ('dbo', 'guest', 'INFORMATION_SCHEMA', 'sys')
"@

                    $missingLogins = Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential -Database $finalDbName -Query $query -ErrorAction Stop
                    foreach ($login in $missingLogins)
                    {
                        $userName = $login.UserName
                        Invoke-sqmLogging -Message "Entferne Windows-User '$userName' (Login existiert nicht mehr)." -FunctionName $functionName -Level "DEBUG"
                        $dropQuery = "DROP USER [$userName]"
                        Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential -Database $finalDbName -Query $dropQuery -ErrorAction SilentlyContinue
                    }
                    Invoke-sqmLogging -Message "Nicht mehr existierende Windows-Logins wurden entfernt." -FunctionName $functionName -Level "INFO"
                    $results += [PSCustomObject]@{ Action = "RemoveOrphanWindowsLogins"; Status = "Success"; Message = "Entfernt: $($missingLogins.Count) User." }
                }
                catch
                {
                    $errMsg = "Fehler beim Entfernen nicht existierender Windows-Logins: $($_.Exception.Message)"
                    Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                    if ($EnableException) { throw }
                    $results += [PSCustomObject]@{ Action = "RemoveOrphanWindowsLogins"; Status = "Failed"; Message = $errMsg }
                }
            }
            else
            {
                $results += [PSCustomObject]@{ Action = "RemoveOrphanWindowsLogins"; Status = "Skipped"; Message = "WhatIf - Entfernen uebersprungen." }
            }

            # ---- 9. 'sa' Konto als Datenbankeigentuemer setzen ----
            $setOwnerAction = "Setze sa-Konto (SID 0x01) als Datenbankeigentuemer fuer '$finalDbName'"
            if ($PSCmdlet.ShouldProcess($finalDbName, $setOwnerAction))
            {
                try
                {
                    Invoke-sqmLogging -Message $setOwnerAction -FunctionName $functionName -Level "INFO"
                    $saNameRow = Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential `
                        -Database 'master' `
                        -Query "SELECT name FROM sys.server_principals WHERE sid = 0x01" `
                        -ErrorAction Stop
                    if (-not $saNameRow -or [string]::IsNullOrWhiteSpace($saNameRow.name))
                    {
                        throw "sa-Login (SID 0x01) nicht gefunden."
                    }
                    $saName = $saNameRow.name
                    Set-DbaDbOwner -SqlInstance $workInstance -SqlCredential $SqlCredential -Database $finalDbName -TargetLogin $saName -ErrorAction Stop
                    Invoke-sqmLogging -Message "Datenbankeigentuemer auf '$saName' gesetzt." -FunctionName $functionName -Level "INFO"
                    $results += [PSCustomObject]@{ Action = "SetDbOwner"; Status = "Success"; Message = "Eigentuemer: $saName" }
                }
                catch
                {
                    $errMsg = "Fehler beim Setzen des Datenbankeigentuemers: $($_.Exception.Message)"
                    Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                    if ($EnableException) { throw }
                    $results += [PSCustomObject]@{ Action = "SetDbOwner"; Status = "Failed"; Message = $errMsg }
                }
            }
            else
            {
                $results += [PSCustomObject]@{ Action = "SetDbOwner"; Status = "Skipped"; Message = "WhatIf - Setzen des Eigentuemers uebersprungen." }
            }

            # AG-Rejoin (Schritt 10) laeuft NICHT mehr hier, sondern im finally-Block weiter unten -
            # damit er garantiert ausgefuehrt wird, auch wenn einer der nachfolgenden (nicht-kritischen)
            # Aufraeumschritte 6-9 mit -EnableException eine Ausnahme durchreicht. Siehe dort.

            # Aufraeumen: temporaere Exportdatei loeschen
            if (Test-Path $userExportFile)
            {
                Remove-Item $userExportFile -Force -ErrorAction SilentlyContinue
            }
        }
        catch
        {
            $errMsg = "Allgemeiner Fehler: $($_.Exception.Message)"
            Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
            if ($EnableException) { throw }
            $results += [PSCustomObject]@{ Action = "GlobalError"; Status = "Failed"; Message = $errMsg }
        }
        finally
        {
            # ---- 10. Datenbank wieder in die AG aufnehmen (inkl. Seeding der Secondaries) ----
            # Laeuft bewusst im finally-Block: sobald der Restore selbst erfolgreich war
            # ($restoreSucceeded), MUSS der Rejoin versucht werden - auch wenn einer der
            # nachfolgenden (nicht-kritischen) Aufraeumschritte 6-9 mit -EnableException eine
            # Ausnahme durchgereicht hat. finally laeuft in PowerShell garantiert, selbst wenn im
            # try/catch ein throw weitergereicht wurde. War $restoreSucceeded nie true (Restore
            # selbst fehlgeschlagen/uebersprungen), wird hier bewusst NICHT versucht, eine
            # moeglicherweise kaputte/nicht vorhandene Datenbank in die AG aufzunehmen.
            #
            # Standardverhalten: eine Datenbank, die aus einer AG entfernt wurde, wird nach dem
            # Restore automatisch wieder aufgenommen, damit die Secondaries per Automatic Seeding
            # neu versorgt werden - sonst bleiben sie nach einem AG-Restore ohne diese Datenbank
            # zurueck. Mit -NoRejoinAvailabilityGroup kann das explizit unterdrueckt werden (z.B. um
            # den Restore erst zu verifizieren, bevor die AG manuell wieder aufgebaut wird).
            Invoke-sqmLogging -Message "AG-Status vor Rejoin-Entscheidung: IsAGDatabase=$isAGDatabase, RestoreSucceeded=$restoreSucceeded, KeepAlwaysOn=$($KeepAlwaysOn.IsPresent), NoRejoinAvailabilityGroup=$($NoRejoinAvailabilityGroup.IsPresent), AvailabilityGroup='$($availabilityGroup.Name)'." -FunctionName $functionName -Level "INFO"

            if ($isAGDatabase -and $restoreSucceeded -and -not $KeepAlwaysOn -and $NoRejoinAvailabilityGroup)
            {
                $skipMsg = "Datenbank '$finalDbName' war Teil der AG '$($availabilityGroup.Name)', wird wegen -NoRejoinAvailabilityGroup NICHT wieder aufgenommen - Secondaries bleiben ohne diese Datenbank zurueck."
                Invoke-sqmLogging -Message $skipMsg -FunctionName $functionName -Level "WARNING"
                $results += [PSCustomObject]@{ Action = "RejoinAG"; Status = "SkippedByRequest"; Message = $skipMsg }
            }

            if ($isAGDatabase -and $restoreSucceeded -and -not $KeepAlwaysOn -and -not $NoRejoinAvailabilityGroup)
            {
                $rejoinAction = "Fuge Datenbank '$finalDbName' wieder in AG '$($availabilityGroup.Name)' ein"
                if ($PSCmdlet.ShouldProcess($finalDbName, $rejoinAction))
                {
                    try
                    {
                        Invoke-sqmLogging -Message $rejoinAction -FunctionName $functionName -Level "INFO"

                        # Pruefe SeedingMode aller Sekundaer-Replikate - stelle Automatic Seeding sicher.
                        # Sekundaer = Name weicht von $primaryInstance ab (siehe Schritt 4) - nicht ueber
                        # "Role -eq 'Secondary'" gefiltert, da die Role transient anders melden kann
                        # (z.B. 'Resolving') und dann Replicas hier stillschweigend uebersprungen wuerden.
                        $agReplicas = Get-DbaAgReplica -SqlInstance $primaryInstance -SqlCredential $SqlCredential `
                            -AvailabilityGroup $availabilityGroup.Name -ErrorAction Stop

                        foreach ($replica in ($agReplicas | Where-Object { $_.Name -ne $primaryInstance }))
                        {
                            if ($replica.SeedingMode -ne 'Automatic')
                            {
                                Invoke-sqmLogging -Message "Replikat '$($replica.Name)': SeedingMode ist '$($replica.SeedingMode)' - stelle auf Automatic um." `
                                    -FunctionName $functionName -Level "INFO"

                                # Primary-Seite: Replikat auf Automatic Seeding umstellen
                                Set-DbaAgReplica -SqlInstance $primaryInstance -SqlCredential $SqlCredential `
                                    -AvailabilityGroup $availabilityGroup.Name `
                                    -Replica $replica.Name `
                                    -SeedingMode Automatic -ErrorAction Stop

                                # Secondary-Seite: GRANT CREATE ANY DATABASE
                                Invoke-DbaQuery -SqlInstance $replica.Name -SqlCredential $SqlCredential `
                                    -Database master `
                                    -Query "ALTER AVAILABILITY GROUP [$($availabilityGroup.Name)] GRANT CREATE ANY DATABASE" `
                                    -ErrorAction SilentlyContinue

                                Invoke-sqmLogging -Message "Replikat '$($replica.Name)' auf Automatic Seeding umgestellt." `
                                    -FunctionName $functionName -Level "INFO"
                                $results += [PSCustomObject]@{ Action = "SetAutoSeeding"; Target = $replica.Name; Status = "Success"; Message = "SeedingMode auf Automatic gesetzt." }
                            }
                            else
                            {
                                Invoke-sqmLogging -Message "Replikat '$($replica.Name)': SeedingMode ist bereits Automatic." `
                                    -FunctionName $functionName -Level "INFO"
                            }
                        }

                        # Datenbank zur AG hinzufuegen
                        Add-DbaAgDatabase -SqlInstance $primaryInstance -SqlCredential $SqlCredential `
                            -AvailabilityGroup $availabilityGroup.Name `
                            -Database $finalDbName `
                            -SeedingMode Automatic `
                            -ErrorAction Stop

                        Invoke-sqmLogging -Message "Datenbank '$finalDbName' erfolgreich in AG '$($availabilityGroup.Name)' aufgenommen." `
                            -FunctionName $functionName -Level "INFO"
                        Write-EventLog -LogName Application -Source $agEventLogSource -EventId 9011 -EntryType Information `
                            -Message "Invoke-sqmRestoreDatabase: '$finalDbName' nach Restore wieder in AG '$($availabilityGroup.Name)' aufgenommen (Automatic Seeding der Secondaries gestartet)." -ErrorAction SilentlyContinue
                        $results += [PSCustomObject]@{ Action = "RejoinAG"; Status = "Success"; Message = "Datenbank in AG '$($availabilityGroup.Name)' aufgenommen (Automatic Seeding)." }
                    }
                    catch
                    {
                        $errMsg = "Fehler beim Wiedereinfuegen in die AG: $($_.Exception.Message)"
                        Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                        # Diese Meldung geht IMMER ins Eventlog (unabhaengig von -EnableException) - eine
                        # nach dem Restore ausserhalb der AG zuruecktbleibende Datenbank (Secondaries ohne
                        # Daten) ist eine kritische Betriebsstoerung und darf nicht nur im sqmSQLTool-Log
                        # verschwinden, wenn niemand $results manuell prueft.
                        Write-EventLog -LogName Application -Source $agEventLogSource -EventId 9012 -EntryType Error `
                            -Message "Invoke-sqmRestoreDatabase: '$finalDbName' konnte nach dem Restore NICHT wieder in AG '$($availabilityGroup.Name)' aufgenommen werden - Secondaries erhalten diese Datenbank NICHT automatisch. Fehler: $errMsg" -ErrorAction SilentlyContinue
                        # Bewusst KEIN erneutes throw hier, selbst mit -EnableException: dies laeuft im
                        # finally-Block, ein throw hier wuerde eine evtl. bereits laufende Exception-
                        # Weiterleitung aus dem try/catch ueberschreiben/verschlucken. Fehlschlag ist im
                        # Eventlog und in $results sichtbar.
                        $results += [PSCustomObject]@{ Action = "RejoinAG"; Status = "Failed"; Message = $errMsg }
                    }
                }
                else
                {
                    $results += [PSCustomObject]@{ Action = "RejoinAG"; Status = "Skipped"; Message = "WhatIf - AG-Wiedereinfuegen uebersprungen." }
                }
            }

            # ---- Datenbank aus Single-User-Modus zuruecknehmen ----
            if ($wasSingleUser)
            {
                $setMultiUserAction = "Setze Datenbank '$finalDbName' zurueck in Multi-User-Modus"
                if ($PSCmdlet.ShouldProcess($finalDbName, $setMultiUserAction))
                {
                    try
                    {
                        $multiUserQuery = "ALTER DATABASE [$finalDbName] SET MULTI_USER;"
                        Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential -Database master -Query $multiUserQuery -ErrorAction Stop
                        Invoke-sqmLogging -Message "Datenbank '$finalDbName' wieder im Multi-User-Modus." -FunctionName $functionName -Level "INFO"
                        $results += [PSCustomObject]@{ Action = "SetMultiUser"; Status = "Success"; Message = "Datenbank wieder im Multi-User-Modus." }
                    }
                    catch
                    {
                        Invoke-sqmLogging -Message "Fehler beim Zuruecksetzen des Multi-User-Modus: $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR"
                        $results += [PSCustomObject]@{ Action = "SetMultiUser"; Status = "Failed"; Message = $_.Exception.Message }
                    }
                }
                else
                {
                    $results += [PSCustomObject]@{ Action = "SetMultiUser"; Status = "Skipped"; Message = "WhatIf - Multi-User uebersprungen." }
                }
            }

            # ---- Policy wiederherstellen ----
            if ($policyWasEnabled -and $policyDeactivated)
            {
                if ($PSCmdlet.ShouldProcess($workInstance, "Policy '$policyName' wieder aktivieren"))
                {
                    Invoke-sqmLogging -Message "Aktiviere Policy '$policyName' wieder." -FunctionName $functionName -Level "INFO"
                    Set-sqmSqlPolicyState -SqlInstance $workInstance -SqlCredential $SqlCredential -Policy $policyName -State Enable -EnableException:$EnableException -Confirm:$false
                    $results += [PSCustomObject]@{ Action = "PolicyReenable"; Status = "Success"; Message = "Policy '$policyName' wieder aktiviert." }
                }
                else
                {
                    $results += [PSCustomObject]@{ Action = "PolicyReenable"; Status = "Skipped"; Message = "WhatIf - Policy-Reaktivierung uebersprungen." }
                }
            }
        }
    }

    end
    {
        Invoke-sqmLogging -Message "$functionName abgeschlossen. $($results.Count) Aktionen protokolliert." -FunctionName $functionName -Level "INFO"
        return $results
    }
}