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 -
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.
 
After a successful restore, the database's live user-access mode is always checked and reset to
MULTI_USER if it isn't already - regardless of whether this function itself ever set
SINGLE_USER. RESTORE DATABASE carries forward whatever user-access mode was in effect on the
source database at backup time (it is part of the database's boot page), so a backup taken while
the source was in SINGLE_USER/RESTRICTED_USER (e.g. as part of a migration process) leaves the
restored copy in that same mode even though this run never touched single-user mode itself.
 
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`. If neither `-BackupFile` nor
`-BackupFiles` is given, pass `-DatabaseName` alone instead - see there.
 
.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 as it appears in the backup file. If `-BackupFile`/`-BackupFiles` is also
given, this is optional and is read directly from the backup's own header via RESTORE HEADERONLY
if omitted - the backup already carries this name, so there is normally no need to type it again
just to restore under the same name.
 
If NEITHER `-BackupFile` NOR `-BackupFiles` is given, `-DatabaseName` becomes REQUIRED and the
most recent backup chain (latest Full plus any Diff/Log backups since) is looked up automatically
from the instance's backup history (`Get-DbaDbBackupHistory -Database <name> -Last`, i.e.
msdb.dbo.backupset) and restored to the most recent possible point - no need to know or type the
actual file path(s) at all. Throws if no backup history exists for that database name.
 
Used for logging, for looking up the existing database of that name (AG membership check,
pre-restore backup, user export, single-user handling - only relevant when -NewDatabaseName is NOT
used), and as the restore target if -NewDatabaseName is not given. It does NOT need to match
anything for the restore itself to work - Restore-DbaDatabase reads the actual logical/physical
file names straight out of the backup file regardless of this parameter (except when it drives the
backup-history lookup above, where it IS the lookup key).
 
.PARAMETER NewDatabaseName
Optional: restores the backup directly as a new/different database, e.g. as a copy alongside the
original (-DatabaseName "arena" -NewDatabaseName "arena_copy" restores the "arena" backup as a
brand-new "arena_copy" - "arena" itself is left completely untouched). This is NOT "restore over
-DatabaseName, then rename" - there is no separate rename step; the restore writes directly to
-NewDatabaseName in one operation, with logical/physical file names based on the new name. Because
of this, once -NewDatabaseName is given, EVERY pre-restore safety check (AG membership, existence,
-BackupBeforeRestore, user export, single-user handling) targets -NewDatabaseName instead of
-DatabaseName - it is -NewDatabaseName that gets overwritten (-WithReplace) if it already exists,
so it is -NewDatabaseName that needs to be backed up/exported/single-usered beforehand, not the
untouched -DatabaseName.
 
.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
# -DatabaseName omitted entirely - read straight from the backup's own header (RESTORE
# HEADERONLY) and restored under that same name. Equivalent to the example above, just without
# having to type the name that's already inside the backup file.
Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -BackupFile "D:\Backup\AdventureWorks.bak"
 
.EXAMPLE
# Neither -BackupFile nor -BackupFiles given - the most recent backup chain (latest Full plus
# any Diff/Log backups since) is looked up automatically from the instance's backup history and
# restored to the most recent possible point. No file path needed at all.
Invoke-sqmRestoreDatabase -SqlInstance "SQL01" -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.
 
For databases whose users are backed by their own SQL Server authentication logins rather than
Windows logins (e.g. an application database with hundreds/thousands of SQL logins such as
"Frontarena"), this function's own orphan-user repair (step 7) only fixes SID mismatches for
logins that already exist by name on the target - it does not know about password changes that
happened on the source since the target's logins were created. Run Sync-sqmDatabaseLogins (or the
Export-sqmDatabaseLogins / Import-sqmDatabaseLogins pair, if source and target cannot reach each
other directly) against the same database right after this function completes to bring those
logins' passwords and SIDs in line with the source.
#>

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, ParameterSetName = 'FromHistory')]
        [Parameter(Mandatory = $false)]
        [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

        # Rueckfragen genesteter Cmdlets zentral unterdruecken, statt an jedem Aufruf einzeln
        # -Confirm:$false zu haengen. Der Umweg ueber den Parameter ist versionsabhaengig und damit
        # zerbrechlich: Export-DbaUser etwa unterstuetzt ShouldProcess in dbatools 2.8.2 nicht, ein
        # -Confirm:$false laesst den Aufruf dort mit "Es wurde kein Parameter gefunden" scheitern.
        # $ConfirmPreference wirkt dagegen auf jedes Cmdlet, egal ob es den Parameter kennt.
        $ConfirmPreference = 'None'

        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" -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()
        $isAGDatabase = $false
        $availabilityGroup = $null
        $primaryInstance = $null
        $workInstance = $null
        $secondaryInstances = @()
        $wasSingleUser = $false
        $originalDbStatus = $null
        $restoreSucceeded = $false

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

        # Bestimme die Liste der Backup-Dateien je nach Parametersatz. Im Set 'FromHistory' (weder
        # -BackupFile noch -BackupFiles angegeben) wird die aktuellste Wiederherstellungskette
        # (neuestes Full + alle seitherigen Diff/Log) automatisch aus der Backup-Historie der
        # Instanz ermittelt - der Aufrufer muss den/die Dateipfad(e) dafuer nicht kennen. Laeuft
        # bewusst hier in begin, nicht erst in process: Get-DbaDbBackupHistory verbindet sich
        # selbststaendig (unabhaengig vom spaeter in process ermittelten $server), ein Fehlschlag
        # soll den Lauf so frueh wie moeglich abbrechen, bevor irgendetwas Zustandsveraenderndes
        # passiert (Policy-Deaktivierung, User-Export, etc.).
        $backupFileList = switch ($PSCmdlet.ParameterSetName)
        {
            'Sequence' { $BackupFiles }
            'FromHistory'
            {
                Invoke-sqmLogging -Message "Weder -BackupFile noch -BackupFiles angegeben - ermittle die aktuellste Wiederherstellungskette aus der Backup-Historie fuer '$DatabaseName' auf '$SqlInstance'." -FunctionName $functionName -Level "INFO"

                $histRows = Get-DbaDbBackupHistory -SqlInstance $SqlInstance -SqlCredential $SqlCredential `
                    -Database $DatabaseName -Last -EnableException -ErrorAction Stop

                if (-not $histRows)
                {
                    $errMsg = "Keine Backup-Historie fuer Datenbank '$DatabaseName' auf '$SqlInstance' gefunden (msdb.dbo.backupset) - -BackupFile oder -BackupFiles muss dann explizit angegeben werden."
                    Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                    throw $errMsg
                }

                # Get-DbaDbBackupHistory -Last liefert eine Zeile pro Backup (Full, ggf. Diff, ggf.
                # mehrere Logs), .Path ist bei gestreiften Backups selbst ein Array - beides hier
                # chronologisch (Start) zu einer flachen Liste zusammenfuehren, in exakt der
                # Reihenfolge, die die 'Sequence'-Restorelogik weiter unten ohnehin erwartet.
                $resolvedFiles = [System.Collections.Generic.List[string]]::new()
                foreach ($histRow in ($histRows | Sort-Object Start)) { foreach ($p in @($histRow.Path)) { $resolvedFiles.Add($p) } }

                Invoke-sqmLogging -Message "Wiederherstellungskette aus Historie ermittelt ($($resolvedFiles.Count) Datei(en)): $($resolvedFiles -join ', ')" -FunctionName $functionName -Level "INFO"
                $resolvedFiles.ToArray()
            }
            default { $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

            # ---- -DatabaseName aus dem Backup-Header lesen, falls nicht angegeben ----
            # -DatabaseName steht bereits im Backup selbst (RESTORE HEADERONLY liest ihn direkt aus der
            # .bak/.trn-Datei) - der Aufrufer muss ihn nicht redundant selbst tippen, wenn ohnehin unter
            # demselben Namen wiederhergestellt werden soll. -DatabaseName bleibt aber weiterhin
            # explizit angebbar (z.B. wenn der Aufrufer den Namen schon kennt und die Instanz nicht
            # extra fuer den Header-Lookup ansprechen will). Bei der 'Sequence'-Parameterset wird nur
            # die ERSTE Datei (das Full-Backup) fuer den Header-Lookup verwendet - die weiteren Dateien
            # sind Diff/Log, keine eigenstaendigen Backups mit eigenem Header. Bei 'SingleFile' kann
            # -BackupFile ein gestreiftes Backup ueber mehrere physische Dateien sein - dort gehoeren
            # ALLE Dateien zum selben Backup-Set und muessen gemeinsam in die DISK-Klausel.
            if (-not $PSBoundParameters.ContainsKey('DatabaseName') -or [string]::IsNullOrWhiteSpace($DatabaseName))
            {
                $headerLookupFiles = if ($PSCmdlet.ParameterSetName -eq 'Sequence') { @($backupFileList[0]) } else { @($backupFileList) }
                $diskClause = ($headerLookupFiles | ForEach-Object { "DISK = N'$($_.Replace("'", "''"))'" }) -join ', '
                try
                {
                    $headerInfo = Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database 'master' `
                        -Query "RESTORE HEADERONLY FROM $diskClause" -EnableException -ErrorAction Stop
                    $detectedName = ($headerInfo | Select-Object -First 1).DatabaseName
                    if ([string]::IsNullOrWhiteSpace($detectedName))
                    {
                        throw "RESTORE HEADERONLY lieferte keinen DatabaseName-Wert."
                    }
                    $DatabaseName = $detectedName
                    Invoke-sqmLogging -Message "-DatabaseName nicht angegeben - aus Backup-Header gelesen: '$DatabaseName'." -FunctionName $functionName -Level "INFO"
                }
                catch
                {
                    $errMsg = "-DatabaseName wurde nicht angegeben und konnte nicht aus dem Backup-Header gelesen werden: $($_.Exception.Message)"
                    Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                    if ($EnableException) { throw }
                    $results += [PSCustomObject]@{ Action = "ReadBackupHeader"; Status = "Failed"; Message = $errMsg }
                    return
                }
            }

            # Name der tatsaechlichen Zieldatenbank auf der Instanz - bei -NewDatabaseName ist das NICHT
            # $DatabaseName (die bleibt unberuehrt liegen), sondern der neue Name. Muss VOR allen
            # Pre-Restore-Pruefungen (AG-Mitgliedschaft, Existenz, BackupBeforeRestore, User-Export,
            # Single-User-Handling) feststehen, denn genau diese Zieldatenbank wird gleich per
            # -WithReplace ueberschrieben - nicht $DatabaseName. Beispiel: -DatabaseName arena
            # -NewDatabaseName arena_copy darf niemals Pruefungen/Sicherung gegen die (unberuehrte)
            # 'arena' fahren, sondern gegen 'arena_copy'.
            $finalDbName = if ($NewDatabaseName) { $NewDatabaseName } else { $DatabaseName }
            $userExportFile = Join-Path $tempDir "UserExport_${finalDbName}_$(Get-Date -Format 'yyyyMMddHHmmss').sql"

            # 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.
            # Mitgliedschaft ueber die Katalogsichten, NICHT ueber SMO mit unterdruecktem Fehler.
            # Get-sqmDatabaseAgMembership unterscheidet "keine AG", "nicht Mitglied" und "konnte ich
            # nicht ermitteln" und wirft im dritten Fall. Genau das fehlte hier: der frueher genutzte
            # Get-DbaAgDatabase-Aufruf mit -ErrorAction SilentlyContinue lieferte bei fehlenden Rechten,
            # einer stolpernden SMO-Enumeration oder einer Verbindung zur falschen Instanz dasselbe
            # leere Ergebnis wie bei einer echten Standalone-Datenbank. Die Funktion lief dann in den
            # Standalone-Pfad, uebersprang das Entfernen aus der AG und scheiterte erst Schritte
            # spaeter an ALTER DATABASE und RESTORE, die SQL Server bei einer AG-Datenbank ablehnt.
            $agMembership = Get-sqmDatabaseAgMembership -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $finalDbName
            if ($agMembership.IsAgDatabase)
            {
                $isAGDatabase = $true
                $agName = $agMembership.AvailabilityGroupName
                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
                if (-not $availabilityGroup)
                {
                    # Die Mitgliedschaft steht per Katalogsicht fest - ohne das AG-Objekt koennten
                    # Entfernen, Secondary-Cleanup und Rejoin aber nicht laufen. Weitermachen wuerde
                    # die Datenbank standalone zuruecklassen, deshalb hier abbrechen.
                    throw "'$finalDbName' ist laut Katalogsicht Mitglied der Availability Group '$agName', das AG-Objekt konnte auf '$SqlInstance' aber nicht geladen werden. Ohne dieses Objekt sind Entfernen und Rejoin nicht moeglich - Abbruch, damit die Datenbank nicht standalone zurueckbleibt."
                }
                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.
                # Ist AlwaysOn auf der Instanz gar nicht aktiviert, gibt es auch keine AG zum Beitreten.
                # Der Aufruf wuerde dann nur die Warnung "Availability Group (HADR) is not configured
                # for the instance" ins Log schreiben, die im Betrieb wie ein Problem aussieht, aber der
                # voellig normale Zustand einer Einzelinstanz ist.
                $instanceAgs = if ($agMembership.HadrEnabled)
                {
                    @(Get-DbaAvailabilityGroup -SqlInstance $SqlInstance -SqlCredential $SqlCredential -ErrorAction SilentlyContinue)
                }
                else { @() }

                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 -EnableException -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))
                {
                    # Zuerst der Wert aus sys.dm_hadr_availability_replica_states (role = 1), den
                    # Get-sqmDatabaseAgMembership oben mitgeliefert hat. Der Rueckfall auf die
                    # verbundene Instanz ist nur die letzte Reissleine: ist -SqlInstance ein
                    # SEKUNDAERReplikat, laufen Restore und ALTER DATABASE dort erneut ins Leere,
                    # also erst alles andere versuchen.
                    if ($agMembership -and -not [string]::IsNullOrWhiteSpace($agMembership.PrimaryReplica))
                    {
                        $primaryInstance = $agMembership.PrimaryReplica
                        Invoke-sqmLogging -Message "AG '$($availabilityGroup.Name)': PrimaryReplicaServerName ist leer - verwende die per DMV ermittelte Primary '$primaryInstance'." -FunctionName $functionName -Level "WARNING"
                    }
                    else
                    {
                        Invoke-sqmLogging -Message "AG '$($availabilityGroup.Name)': PrimaryReplicaServerName ist leer und die Primary liess sich auch per DMV nicht ermitteln (AG evtl. gerade im Failover) - falle zurueck auf verbundene Instanz '$SqlInstance'. Ist das ein Sekundaerreplikat, schlagen Restore und ALTER DATABASE fehl." -FunctionName $functionName -Level "WARNING"
                        $primaryInstance = $SqlInstance
                    }
                    $primaryShortName = ($primaryInstance -split '[.\\]')[0]
                }
                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
            }

            # ---- Falls AlwaysOn: Datenbank aus der AG entfernen (primaer) und von sekundaeren Replikaten loeschen ----
            # WICHTIG: Muss VOR jeder direkten ALTER DATABASE-Operation laufen (Normalize-to-Multi-User und
            # Single-User-Schritt weiter unten) - SQL Server lehnt ALTER DATABASE SET SINGLE_USER/MULTI_USER/OFFLINE
            # etc. grundsaetzlich ab, solange die Datenbank noch Mitglied einer Availability Group ist ('...cannot
            # be performed on database ... because it is involved in a database mirroring session or an availability
            # group'). Frueher lief dieser Schritt nach der Single-User-Behandlung, wodurch genau dieser Fehler
            # ausgeloest wurde, sobald die Datenbank aktive Verbindungen hatte oder -ForceSingleUser gesetzt war.
            # Primary/Secondaries wurden bereits oben (Arbeits-Instanz-Ermittlung) bestimmt.
            if ($isAGDatabase -and -not $KeepAlwaysOn)
            {
                if (-not $agMembership.IsAgDatabase)
                {
                    # 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 '$finalDbName' aus der AG '$($availabilityGroup.Name)'"
                    if ($PSCmdlet.ShouldProcess($finalDbName, $removeAgAction))
                    {
                        try
                        {
                            Invoke-sqmLogging -Message $removeAgAction -FunctionName $functionName -Level "INFO"
                            Remove-DbaAgDatabase -SqlInstance $primaryInstance -SqlCredential $SqlCredential -AvailabilityGroup $availabilityGroup.Name -Database $finalDbName -Confirm:$false -EnableException -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 '$finalDbName' 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 '$finalDbName' auf sekundaerem Knoten '$secondary'"
                    if ($PSCmdlet.ShouldProcess($finalDbName, $removeDbAction))
                    {
                        try
                        {
                            Invoke-sqmLogging -Message $removeDbAction -FunctionName $functionName -Level "INFO"
                            # -NonPooledConnection: dbatools cached SMO-Verbindungen werden innerhalb der Session
                            # wiederverwendet: eine bereits frueher in dieser Session geladene .Databases-Collection
                            # bleibt sonst auf dem alten Stand stehen, selbst wenn sich die Datenbank auf dem
                            # Server laengst geaendert hat (z.B. bereits geloescht). Ohne frische Verbindung wuerde
                            # die Pruefung unten faelschlich "vorhanden" melden.
                            $secondaryServer = Connect-DbaInstance -SqlInstance $secondary -SqlCredential $SqlCredential -NonPooledConnection -ErrorAction Stop
                            if ($secondaryServer.Databases[$finalDbName])
                            {
                                # Nach Remove-DbaAgDatabase liegt die Kopie auf dem Secondary meist im Status
                                # RESTORING (IsAccessible = $false) - trotzdem vorhanden und muss geloescht
                                # werden, sonst schlaegt spaeter Add-DbaAgDatabase/Automatic Seeding mit
                                # "Database With Name Already Exists" fehl. Daher NICHT auf IsAccessible pruefen.
                                #
                                # Remove-DbaDatabase wirft bei einem fehlgeschlagenen Drop pro Datenbank KEINE
                                # Exception (auch nicht mit -EnableException) - es faengt den Fehler intern und
                                # packt den rohen SQL-Fehlertext in die Status-Eigenschaft des Rueckgabeobjekts.
                                # Deshalb muss der Rueckgabewert explizit ausgewertet werden statt "keine Exception
                                # = Erfolg" anzunehmen.
                                $dropResult = Remove-DbaDatabase -SqlInstance $secondary -SqlCredential $SqlCredential -Database $finalDbName -Confirm:$false -EnableException -ErrorAction Stop
                                if ($dropResult -and $dropResult.Status -eq 'Dropped')
                                {
                                    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." }
                                }
                                elseif ($dropResult -and $dropResult.Status -match 'does not exist')
                                {
                                    # Ziel bereits erreicht (Datenbank ist weg) - kein Fehler, nur die zuvor
                                    # gecachte Sicht war veraltet.
                                    Invoke-sqmLogging -Message "Datenbank auf '$secondary' war beim tatsaechlichen Drop bereits nicht mehr vorhanden." -FunctionName $functionName -Level "INFO"
                                    $results += [PSCustomObject]@{ Action = "RemoveFromSecondary"; Target = $secondary; Status = "AlreadyGone"; Message = "Datenbank war auf sekundaerem Knoten bereits nicht mehr vorhanden." }
                                }
                                else
                                {
                                    $dropErrMsg = "Fehler beim Loeschen auf '$secondary': $($dropResult.Status)"
                                    Invoke-sqmLogging -Message $dropErrMsg -FunctionName $functionName -Level "ERROR"
                                    if ($EnableException) { throw $dropErrMsg }
                                    $results += [PSCustomObject]@{ Action = "RemoveFromSecondary"; Target = $secondary; Status = "Failed"; Message = $dropErrMsg }
                                }
                            }
                            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." }
                    }
                }
            }

            # ---- Vorbereitung: Policy temporaer deaktivieren (auf der Arbeits-Instanz) ----
            if (-not [string]::IsNullOrWhiteSpace($policyName))
            {
                try
                {
                    # Policy-Status per Katalogsicht statt Get-DbaPbmPolicy: die dbatools-PBM-Cmdlets
                    # setzen den SMO-PolicyStore voraus und brechen unter PowerShell 7 ab
                    # ("Get-DbaPbmStore: This command is not supported on Linux or macOS"). Zusammen
                    # mit -ErrorAction SilentlyContinue war das ein STILLER Fehlschlag: $policyObj
                    # blieb leer, der gesamte Block wurde uebersprungen und die Policy waehrend des
                    # Restores nie deaktiviert - ohne jeden Hinweis im Log. Die Katalogsicht ist
                    # reines T-SQL und verhaelt sich unter PS 5.1 und PS 7 identisch.
                    $policyNameLit = $policyName -replace "'", "''"
                    $policyRow = Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential -Database msdb `
                        -Query "SELECT is_enabled FROM msdb.dbo.syspolicy_policies WHERE name = N'$policyNameLit';" -EnableException -ErrorAction Stop
                    if ($policyRow -and [int]$policyRow.is_enabled -eq 1)
                    {
                        $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[$finalDbName]
            $dbExists = $targetDb -ne $null

            if ($dbExists)
            {
                Invoke-sqmLogging -Message "Datenbank '$finalDbName' 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 '$finalDbName' 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($finalDbName, $normalizeAction))
                    {
                        try
                        {
                            Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential -Database master `
                                -Query "ALTER DATABASE [$finalDbName] SET MULTI_USER WITH ROLLBACK IMMEDIATE;" -EnableException -ErrorAction Stop
                            $wasSingleUser = $true
                            $targetDb.Refresh()
                            Invoke-sqmLogging -Message "Datenbank '$finalDbName' 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 '$finalDbName' 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 = "${finalDbName}_preRestore_$(Get-Date -Format 'yyyyMMdd_HHmmss').bak"
                $backupFileFull = Join-Path (Get-DbaDefaultPath -SqlInstance $workInstance -SqlCredential $SqlCredential).Backup $backupFileName
                $backupParams = @{
                    SqlInstance        = $workInstance
                    SqlCredential    = $SqlCredential
                    Database        = $finalDbName
                    Path            = $backupFileFull
                    Type            = 'Full'
                    Confirm            = $false
                    EnableException = $true
                    ErrorAction        = 'Stop'
                }
                if ($PSCmdlet.ShouldProcess($finalDbName, "Backup der Datenbank '$finalDbName' 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($finalDbName, "Export der Datenbank-User nach $userExportFile"))
                {
                    try
                    {
                        Invoke-sqmLogging -Message "Exportiere User der Datenbank '$finalDbName' nach $userExportFile" -FunctionName $functionName -Level "INFO"
                        # KEIN -Confirm:$false: Export-DbaUser unterstuetzt ShouldProcess nicht (geprueft
                        # gegen dbatools 2.8.2) und wirft dann "Es wurde kein Parameter gefunden, der dem
                        # Parameternamen 'Confirm' entspricht". Der catch unten beendet den Lauf mit
                        # return - der Restore ist danach also gar nicht mehr gelaufen. Unterdrueckte
                        # Rueckfragen kommen fuer dieses Cmdlet ueber $ConfirmPreference = 'None' im
                        # begin-Block, das wirkt unabhaengig davon, ob ein Cmdlet den Parameter kennt.
                        Export-DbaUser -SqlInstance $workInstance -SqlCredential $SqlCredential -Database $finalDbName -FilePath $userExportFile -EnableException -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 '$finalDbName' hat $activeConnections aktive Verbindungen. Setze in Single-User-Modus." -FunctionName $functionName -Level "INFO"
                }
                else
                {
                    Invoke-sqmLogging -Message "Erzwinge Single-User-Modus fuer Datenbank '$finalDbName'." -FunctionName $functionName -Level "INFO"
                }
                $setSingleUserAction = "Setze Datenbank '$finalDbName' in Single-User-Modus"
                if ($PSCmdlet.ShouldProcess($finalDbName, $setSingleUserAction))
                {
                    try
                    {
                        $singleUserQuery = "ALTER DATABASE [$finalDbName] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;"
                        Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential -Database master -Query $singleUserQuery -EnableException -ErrorAction Stop
                        $wasSingleUser = $true
                        Invoke-sqmLogging -Message "Datenbank '$finalDbName' 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 '$finalDbName' hat keine aktiven Verbindungen." -FunctionName $functionName -Level "INFO"
            }

            # ---- 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 wurde bereits im begin-Block bestimmt (vor allen Pre-Restore-
            # Pruefungen), hier nicht erneut berechnen.
            $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)
                    Confirm            = $false
                    EnableException = $true
                    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'" `
                            -EnableException -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
                        # WICHTIG (1.9.35.0): -EnableException oben im $restoreParams-Hash ist zwingend -
                        # ohne diesen Parameter meldet Restore-DbaDatabase interne Ablehnungen (z.B. "RESTORE
                        # cannot operate on database ... because it is ... an availability group") nur als
                        # PSFramework-Warning und gibt $null zurueck, OHNE eine Exception zu werfen. -ErrorAction
                        # Stop allein faengt das NICHT ab, weil dabei kein regulaerer PowerShell-Fehlerdatensatz
                        # entsteht. Das war exakt der Grund, warum ein echter Lauf (SFCSDBS103IHZ, Restore aus
                        # F:\DB_Transfer_Prod\*.bak) "Restore erfolgreich" protokollierte, obwohl in
                        # msdb.dbo.restorehistory ueberhaupt kein neuer Eintrag auftauchte - der eigentliche
                        # RESTORE-Befehl war nie gelaufen. Zusaetzlich zur Exception hier auch das Ergebnis
                        # selbst pruefen, statt uns allein auf "es wurde nichts geworfen" zu verlassen.
                        if (-not $restoreResult)
                        {
                            throw "Restore-DbaDatabase lieferte kein Ergebnis zurueck (Restore vermutlich nicht ausgefuehrt)."
                        }
                        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 -EnableException -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 -Confirm:$false -EnableException -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 -EnableException -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" `
                        -EnableException -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 -Confirm:$false -EnableException -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"

                        # Eine AG verlangt zwingend Full Recovery mit einer luecklosen Log-Chain. Ein
                        # Restore aus einem Backup, das unter SIMPLE gezogen wurde (oder dessen Recovery
                        # Model beim Restore vom Quellsystem uebernommen wurde), kommt sonst als SIMPLE
                        # wieder hoch und Add-DbaAgDatabase schlaegt zuverlaessig mit "RecoveryModel of
                        # database [...] is not Full, but Simple" fehl. SET RECOVERY FULL allein reicht
                        # nicht: bis zum naechsten FULL-Backup danach bleibt die Log-Chain unterbrochen
                        # (SQL Server verhaelt sich bis dahin weiterhin wie SIMPLE, sog. "pseudo-simple") -
                        # deshalb hier direkt danach ein FULL-Backup anstossen, bevor Add-DbaAgDatabase
                        # ueberhaupt versucht wird.
                        $currentRecoveryModel = (Invoke-DbaQuery -SqlInstance $primaryInstance -SqlCredential $SqlCredential -Database master `
                                -Query "SELECT recovery_model_desc FROM sys.databases WHERE name = N'$($finalDbName.Replace("'", "''"))'" `
                                -EnableException -ErrorAction Stop).recovery_model_desc

                        if ($currentRecoveryModel -ne 'FULL')
                        {
                            Invoke-sqmLogging -Message "Datenbank '$finalDbName' hat Recovery Model '$currentRecoveryModel' - fuer AG-Mitgliedschaft wird FULL benoetigt. Stelle um und erstelle ein FULL-Backup." `
                                -FunctionName $functionName -Level "INFO"

                            Invoke-DbaQuery -SqlInstance $primaryInstance -SqlCredential $SqlCredential -Database master `
                                -Query "ALTER DATABASE [$finalDbName] SET RECOVERY FULL" -EnableException -ErrorAction Stop

                            $null = Backup-DbaDatabase -SqlInstance $primaryInstance -SqlCredential $SqlCredential `
                                -Database $finalDbName -Type Full -CompressBackup -EnableException -ErrorAction Stop

                            Invoke-sqmLogging -Message "Recovery Model von '$finalDbName' auf FULL umgestellt und FULL-Backup erstellt." `
                                -FunctionName $functionName -Level "INFO"
                            $results += [PSCustomObject]@{ Action = "EnsureFullRecoveryModel"; Status = "Success"; Message = "Recovery Model war '$currentRecoveryModel' - auf FULL umgestellt und FULL-Backup erstellt." }
                        }

                        # 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 -EnableException -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 -Confirm:$false -EnableException -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 `
                            -Confirm:$false `
                            -EnableException `
                            -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/Restricted-User-Modus zuruecknehmen ----
            # WICHTIG: Nicht auf die eigene $wasSingleUser-Fahne verlassen - die pruefte bislang nur,
            # ob DIESE Funktion selbst vor dem Restore SINGLE_USER gesetzt hatte. RESTORE DATABASE
            # uebernimmt aber den User-Access-Modus (MULTI_USER/SINGLE_USER/RESTRICTED_USER), der zum
            # Zeitpunkt der Datensicherung im Backup selbst gesetzt war (steckt in der Boot-Page der
            # Datenbank). Wurde das Backup von einer Quelle gezogen, die z.B. fuer eine Migration
            # bewusst auf RESTRICTED_USER stand (genau der Fall bei Dateien wie
            # F:\DB_Transfer_Prod\*.bak), kommt die wiederhergestellte Datenbank in genau diesem Modus
            # wieder hoch - unabhaengig davon, ob $wasSingleUser hier je $true war. Ein sysadmin-Login
            # (unter dem diese Funktion laut NOTES laufen muss) kann sich trotzdem verbinden, weshalb
            # die Schritte 6-10 anstandslos durchlaufen und die Datenbank am Ende unbemerkt in
            # RESTRICTED_USER zurueckbleibt. Deshalb hier immer den TATSAECHLICHEN Live-Zustand
            # abfragen und bei Bedarf zuruecksetzen, statt sich auf die eigene Vorgeschichte zu
            # verlassen.
            if ($restoreSucceeded)
            {
                $liveAccess = $null
                try
                {
                    $finalDbNameEscaped = $finalDbName.Replace("'", "''")
                    $liveAccess = (Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential -Database master `
                        -Query "SELECT user_access_desc FROM sys.databases WHERE name = N'$finalDbNameEscaped'" -EnableException -ErrorAction Stop).user_access_desc
                }
                catch
                {
                    Invoke-sqmLogging -Message "Konnte den aktuellen User-Access-Modus von '$finalDbName' nicht ermitteln: $($_.Exception.Message)" -FunctionName $functionName -Level "WARNING"
                }

                if ($liveAccess -and $liveAccess -ne 'MULTI_USER')
                {
                    $setMultiUserAction = "Datenbank '$finalDbName' ist nach dem Restore im Modus '$liveAccess' (aus dem Backup uebernommen) - setze zurueck auf Multi-User-Modus"
                    if ($PSCmdlet.ShouldProcess($finalDbName, $setMultiUserAction))
                    {
                        try
                        {
                            Invoke-sqmLogging -Message $setMultiUserAction -FunctionName $functionName -Level "WARNING"
                            $multiUserQuery = "ALTER DATABASE [$finalDbName] SET MULTI_USER;"
                            Invoke-DbaQuery -SqlInstance $workInstance -SqlCredential $SqlCredential -Database master -Query $multiUserQuery -EnableException -ErrorAction Stop
                            Invoke-sqmLogging -Message "Datenbank '$finalDbName' wieder im Multi-User-Modus." -FunctionName $functionName -Level "INFO"
                            $results += [PSCustomObject]@{ Action = "SetMultiUser"; Status = "Success"; Message = "War '$liveAccess', auf Multi-User-Modus zurueckgesetzt." }
                        }
                        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
    }
}